diff --git a/.gitignore b/.gitignore index ad1b078..78d257c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ *~ *.log *.pyc -usb __pycache__ worker/fpgamining/firmware/* config/* diff --git a/modules/rph/__init__.py b/modules/rph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/rph/usb/__init__.py b/modules/rph/usb/__init__.py new file mode 100644 index 0000000..7089c0a --- /dev/null +++ b/modules/rph/usb/__init__.py @@ -0,0 +1,4 @@ +from .rphusbworker import rphUSBWorker +from .rphusbhotplug import rphUSBHotplugWorker + +workerclasses = [rphUSBWorker, rphUSBHotplugWorker] diff --git a/modules/rph/usb/boardproxy.py b/modules/rph/usb/boardproxy.py new file mode 100644 index 0000000..1b626e0 --- /dev/null +++ b/modules/rph/usb/boardproxy.py @@ -0,0 +1,183 @@ +# Modular Python Bitcoin Miner +# Copyright (C) 2012 Michael Sparmann (TheSeven) +# Copyright (C) 2011-2012 rphlx +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Please consider donating to 1PLAPWDejJPJnY2ppYCgtw5ko8G5Q4hPzh if you +# want to support further development of the Modular Python Bitcoin Miner. + + + +############################################################### +# rph usb out of process board access dispatcher # +############################################################### + + + +import time +import signal +import struct +import traceback +from threading import Thread, Condition, RLock +from multiprocessing import Process +from core.job import Job +from .driver import rphUSBDevice + + +class rphUSBBoardProxy(Process): + + + def __init__(self, rxconn, txconn, serial, takeover, firmware, pollinterval): + super(rphUSBBoardProxy, self).__init__() + self.rxconn = rxconn + self.txconn = txconn + self.serial = serial + self.takeover = takeover + self.firmware = firmware + self.pollinterval = pollinterval + + + def run(self): + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + self.lock = RLock() + self.wakeup = Condition() + self.error = None + self.pollingthread = None + self.shutdown = False + self.job = None + self.checklockout = 0 + self.lastnonce = 0 + self.multiplier = 0 + + try: + + # Listen for setup commands + while True: + data = self.rxconn.recv() + + if data[0] == "connect": break + + else: raise Exception("Unknown setup message: %s" % str(data)) + + # Connect to board and upload firmware if neccessary + self.device = rphUSBDevice(self, self.serial, self.takeover, self.firmware) + + # Configure clock + #self._set_multiplier(192000000) + self.send("speed_changed", 16*192000000) + + # Start polling thread + self.pollingthread = Thread(None, self.polling_thread, "polling_thread") + self.pollingthread.daemon = True + self.pollingthread.start() + + self.send("started_up") + + # Listen for commands + while True: + if self.error: raise self.error + + data = self.rxconn.recv() + + if data[0] == "shutdown": break + + elif data[0] == "ping": self.send("pong") + + elif data[0] == "pong": pass + + elif data[0] == "set_pollinterval": + self.pollinterval = data[1] + with self.wakeup: self.wakeup.notify() + + elif data[0] == "send_job": + self.checklockout = time.time() + 1 + self.job = data[1] + with self.wakeup: + start = time.time() + self.device.send_job(data[2][::-1] + data[1][75:63:-1]) + end = time.time() + self.lastnonce = 0 + self.checklockout = end + 0.5 + self.respond(start, end) + + else: raise Exception("Unknown message: %s" % str(data)) + + except: self.log("Exception caught: %s" % traceback.format_exc(), 100, "r") + finally: + self.shutdown = True + with self.wakeup: self.wakeup.notify() + try: self.pollingthread.join(2) + except: pass + self.send("dying") + + + def send(self, *args): + with self.lock: self.txconn.send(args) + + + def respond(self, *args): + self.send("response", *args) + + + def log(self, message, loglevel, format = ""): + self.send("log", message, loglevel, format) + + + def polling_thread(self): + try: + lastshares = [] + counter = 0 + + while not self.shutdown: + # Poll for nonces + now = time.time() + nonces = self.device.read_nonces() + exhausted = False + with self.wakeup: + if len(nonces) > 0 and nonces[0] < self.lastnonce: + self.lastnonce = nonces[0] + exhausted = True + if exhausted: self.send("keyspace_exhausted") + for nonce in nonces: + if not nonce[0] in lastshares: + if self.job: self.send("nonce_found", time.time(), struct.pack(" len(nonces): lastshares.pop(0) + + counter += 1 + if counter >= 10: + counter = 0 + # Read temperatures + temp = self.device.read_temps() + self.send("temperature_read", temp) + + with self.wakeup: self.wakeup.wait(self.pollinterval) + + except Exception as e: + self.log("Exception caught: %s" % traceback.format_exc(), 100, "r") + self.error = e + # Unblock main thread + self.send("ping") + + + def _set_multiplier(self, multiplier): + multiplier = min(max(multiplier, 1), self.device.maximum_multiplier) + if multiplier == self.multiplier: return + self.device.set_multiplier(multiplier) + self.multiplier = multiplier + self.checklockout = time.time() + 2 + self.send("speed_changed", (multiplier + 1) * self.device.base_frequency * self.device.hashes_per_clock) diff --git a/modules/rph/usb/driver.py b/modules/rph/usb/driver.py new file mode 100644 index 0000000..1a08905 --- /dev/null +++ b/modules/rph/usb/driver.py @@ -0,0 +1,131 @@ +# Modular Python Bitcoin Miner +# Copyright (C) 2012 Michael Sparmann (TheSeven) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Please consider donating to 1PLAPWDejJPJnY2ppYCgtw5ko8G5Q4hPzh if you +# want to support further development of the Modular Python Bitcoin Miner. + + + +######################################### +# rph usb level driver # +######################################### + + + +import time +import usb +import struct +import traceback +from array import array +from threading import RLock +from binascii import hexlify, unhexlify + + + +class rphUSBDevice(object): + + + def __init__(self, proxy, serial, takeover, firmware): + self.lock = RLock() + self.proxy = proxy + self.serial = serial + self.takeover = takeover + self.firmware = firmware + self.handle = None + self.rxdata = array("B") + permissionproblem = False + deviceinuse = False + for bus in usb.busses(): + if self.handle != None: break + for dev in bus.devices: + if self.handle != None: break + if dev.idVendor == 0xf1f0: + try: + handle = dev.open() + _serial = hexlify(handle.controlMsg(0xc0, 0x96, 16, 0, 0, 100)) + #_serial = "0" # handle.getString(dev.iSerialNumber, 100).decode("latin1") + if serial == "" or serial == _serial: + try: + if self.takeover: + handle.reset() + time.sleep(1) + configuration = dev.configurations[0] + interface = configuration.interfaces[0][0] + handle.setConfiguration(configuration.value) + handle.claimInterface(interface.interfaceNumber) + handle.setAltInterface(interface.alternateSetting) + self.handle = handle + self.serial = _serial + except: deviceinuse = True + except: permissionproblem = True + if self.handle == None: + if deviceinuse: + raise Exception("Can not open the specified device, possibly because it is already in use") + if permissionproblem: + raise Exception("Can not open the specified device, possibly due to insufficient permissions") + raise Exception("Can not open the specified device") + + + def set_multiplier(self, multiplier): + return None + #with self.lock: + # self.handle.controlMsg(0x40, 0x83, b"", multiplier, 0, 100) + + + def send_job(self, data): + with self.lock: + self.handle.controlMsg(0x40, 0x40, unhexlify("40") + data, 0, 0, 100) + + + def read_nonces(self): + with self.lock: + char = '' + try: + char = self.handle.controlMsg(0xc0, 0x41, 1, 0, 0, 100) + except: + time.sleep(0.01) + pass + if len(char): + self.rxdata = self.rxdata + char + nonces = [] + if len(self.rxdata) >= 4: + golden = self.rxdata[0:4] + golden = golden[::-1] + golden = golden.tostring() + nonces.append(struct.unpack(" +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Please consider donating to 1PLAPWDejJPJnY2ppYCgtw5ko8G5Q4hPzh if you +# want to support further development of the Modular Python Bitcoin Miner. + + + +################################################### +# rph usb hotplug controller module # +# https://bitcointalk.org/index.php?topic=44891.0 # +################################################### + + + +import traceback +from binascii import hexlify +from threading import Condition, Thread +from core.baseworker import BaseWorker +from .rphusbworker import rphUSBWorker + + + +# Worker main class, referenced from __init__.py +class rphUSBHotplugWorker(BaseWorker): + + version = "rph.usb hotplug manager v0.0.1" + default_name = "rph usb hotplug manager" + can_autodetect = True + settings = dict(BaseWorker.settings, **{ + #"takeover": {"title": "Reset board if it appears to be in use", "type": "boolean", "position": 1200}, + #"firmware": {"title": "Firmware file location", "type": "string", "position": 1400}, + "blacklist": { + "title": "Board list type", + "type": "enum", + "values": [ + {"value": True, "title": "Blacklist"}, + {"value": False, "title": "Whitelist"}, + ], + "position": 2000 + }, + "boards": { + "title": "Board list", + "type": "list", + "element": {"title": "Serial number", "type": "string"}, + "position": 2100 + }, + "scaninterval": {"title": "Bus scan interval", "type": "float", "position": 2200}, + "jobinterval": {"title": "Job interval", "type": "float", "position": 5100}, + "pollinterval": {"title": "Poll interval", "type": "float", "position": 5200}, + }) + + + @classmethod + def autodetect(self, core): + try: + found = False + try: + import usb + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == 0xf1f0: + try: + handle = dev.open() + serial = hexlify(handle.controlMsg(0xc0, 0x96, 16, 0, 0, 100)) + try: + configuration = dev.configurations[0] + interface = configuration.interfaces[0][0] + handle.setConfiguration(configuration.value) + handle.claimInterface(interface.interfaceNumber) + handle.releaseInterface() + handle.setConfiguration(0) + found = True + break + except: pass + except: pass + if found: break + except: pass + if found: core.add_worker(self(core)) + except: pass + + + # Constructor, gets passed a reference to the miner core and the saved worker state, if present + def __init__(self, core, state = None): + # Initialize bus scanner wakeup event + self.wakeup = Condition() + + # Let our superclass do some basic initialization and restore the state if neccessary + super(rphUSBHotplugWorker, self).__init__(core, state) + + + # Validate settings, filling them with default values if neccessary. + # Called from the constructor and after every settings change. + def apply_settings(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBHotplugWorker, self).apply_settings() + if not "serial" in self.settings: self.settings.serial = None + if not "takeover" in self.settings: self.settings.takeover = True + if not "firmware" in self.settings or not self.settings.firmware: + self.settings.firmware = "modules/ztex/firmware/" + if not "blacklist" in self.settings: self.settings.blacklist = True + if self.settings.blacklist == "false": self.settings.blacklist = False + else: self.settings.blacklist = not not self.settings.blacklist + if not "boards" in self.settings: self.settings.boards = [] + if not "jobinterval" in self.settings or not self.settings.jobinterval: self.settings.jobinterval = 60 + if not "pollinterval" in self.settings or not self.settings.pollinterval: self.settings.pollinterval = 0.1 + if not "scaninterval" in self.settings or not self.settings.scaninterval: self.settings.scaninterval = 10 + # Push our settings down to our children + fields = ["takeover", "firmware", "jobinterval", "pollinterval"] + for child in self.children: + for field in fields: child.settings[field] = self.settings[field] + child.apply_settings() + # Rescan the bus immediately to apply the new settings + with self.wakeup: self.wakeup.notify() + + + # Reset our state. Called both from the constructor and from self.start(). + def _reset(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBHotplugWorker, self)._reset() + # These need to be set here in order to make the equality check in apply_settings() happy, + # when it is run before starting the module for the first time. (It is called from the constructor.) + + + # Start up the worker module. This is protected against multiple calls and concurrency by a wrapper. + def _start(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBHotplugWorker, self)._start() + # Initialize child map + self.childmap = {} + # Reset the shutdown flag for our threads + self.shutdown = False + # Start up the main thread, which handles pushing work to the device. + self.mainthread = Thread(None, self.main, self.settings.name + "_main") + self.mainthread.daemon = True + self.mainthread.start() + + + # Shut down the worker module. This is protected against multiple calls and concurrency by a wrapper. + def _stop(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBHotplugWorker, self)._stop() + # Set the shutdown flag for our threads, making them terminate ASAP. + self.shutdown = True + # Trigger the main thread's wakeup flag, to make it actually look at the shutdown flag. + with self.wakeup: self.wakeup.notify() + # Wait for the main thread to terminate. + self.mainthread.join(10) + # Shut down child workers + while self.children: + child = self.children.pop(0) + try: + self.core.log(self, "Shutting down worker %s...\n" % (child.settings.name), 800) + child.stop() + except Exception as e: + self.core.log(self, "Could not stop worker %s: %s\n" % (child.settings.name, traceback.format_exc()), 100, "rB") + + + # Main thread entry point + # This thread is responsible for scanning for boards and spawning worker modules for them + def main(self): + # Loop until we are shut down + while not self.shutdown: + + import usb + + try: + boards = {} + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == 0xf1f0: + try: + handle = dev.open() + serial = hexlify(handle.controlMsg(0xc0, 0x96, 16, 0, 0, 100)) + try: + configuration = dev.configurations[0] + interface = configuration.interfaces[0][0] + handle.setConfiguration(configuration.value) + handle.claimInterface(interface.interfaceNumber) + handle.releaseInterface() + handle.setConfiguration(0) + available = True + except: available = False + boards[serial] = available + except: pass + + for serial in boards.keys(): + if self.settings.blacklist: + if serial in self.settings.boards: del boards[serial] + else: + if serial not in self.settings.boards: del boards[serial] + + kill = [] + for serial, child in self.childmap.items(): + if not serial in boards: + kill.append((serial, child)) + + for serial, child in kill: + try: + self.core.log(self, "Shutting down worker %s...\n" % (child.settings.name), 800) + child.stop() + except Exception as e: + self.core.log(self, "Could not stop worker %s: %s\n" % (child.settings.name, traceback.format_exc()), 100, "rB") + childstats = child.get_statistics() + fields = ["ghashes", "jobsaccepted", "jobscanceled", "sharesaccepted", "sharesrejected", "sharesinvalid"] + for field in fields: self.stats[field] += childstats[field] + try: self.child.destroy() + except: pass + del self.childmap[serial] + try: self.children.remove(child) + except: pass + + for serial, available in boards.items(): + if serial in self.childmap: continue + if not available and self.settings.takeover: + try: + for bus in usb.busses(): + if available: break + for dev in bus.devices: + if available: break + if dev.idVendor == 0xf1f0: + handle = dev.open() + _serial = hexlify(handle.controlMsg(0xc0, 0x96, 16, 0, 0, 100)) + if _serial == serial: + handle.reset() + time.sleep(1) + configuration = dev.configurations[0] + interface = configuration.interfaces[0][0] + handle.setConfiguration(configuration.value) + handle.claimInterface(interface.interfaceNumber) + handle.releaseInterface() + handle.setConfiguration(0) + handle.reset() + time.sleep(1) + available = True + except: pass + if available: + child = rphUSBWorker(self.core) + child.settings.name = "rph USB " + serial + child.settings.serial = serial + fields = ["takeover", "firmware", "jobinterval", "pollinterval"] + for field in fields: child.settings[field] = self.settings[field] + child.apply_settings() + self.childmap[serial] = child + self.children.append(child) + try: + self.core.log(self, "Starting up worker %s...\n" % (child.settings.name), 800) + child.start() + except Exception as e: + self.core.log(self, "Could not start worker %s: %s\n" % (child.settings.name, traceback.format_exc()), 100, "rB") + + except: self.core.log(self, "Caught exception: %s\n" % traceback.format_exc(), 100, "rB") + + with self.wakeup: self.wakeup.wait(self.settings.scaninterval) diff --git a/modules/rph/usb/rphusbworker.py b/modules/rph/usb/rphusbworker.py new file mode 100644 index 0000000..b934aa7 --- /dev/null +++ b/modules/rph/usb/rphusbworker.py @@ -0,0 +1,391 @@ +# Modular Python Bitcoin Miner +# Copyright (C) 2012 Michael Sparmann (TheSeven) +# Copyright (C) 2011-2012 rphlx +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Please consider donating to 1PLAPWDejJPJnY2ppYCgtw5ko8G5Q4hPzh if you +# want to support further development of the Modular Python Bitcoin Miner. + + + +################################################### +# rph usb worker interface module # +# https://bitcointalk.org/index.php?topic=44891.0 # +################################################### + + + +import time +import traceback +from multiprocessing import Pipe +from threading import RLock, Condition, Thread +from core.baseworker import BaseWorker +from .boardproxy import rphUSBBoardProxy +try: from queue import Queue +except: from Queue import Queue + + + +# Worker main class, referenced from __init__.py +class rphUSBWorker(BaseWorker): + + version = "rph.usb worker v0.0.1" + default_name = "Untitled rph USB worker" + settings = dict(BaseWorker.settings, **{ + "serial": {"title": "Board serial number", "type": "string", "position": 1000}, + #"takeover": {"title": "Reset board if it appears to be in use", "type": "boolean", "position": 1200}, + #"firmware": {"title": "Firmware base path", "type": "string", "position": 1400}, + "jobinterval": {"title": "Job interval", "type": "float", "position": 4100}, + "pollinterval": {"title": "Poll interval", "type": "float", "position": 4200}, + }) + + + # Constructor, gets passed a reference to the miner core and the saved worker state, if present + def __init__(self, core, state = None): + # Let our superclass do some basic initialization and restore the state if neccessary + super(rphUSBWorker, self).__init__(core, state) + + # Initialize proxy access locks and wakeup event + self.lock = RLock() + self.transactionlock = RLock() + self.wakeup = Condition() + self.workloopwakeup = Condition() + + + # Validate settings, filling them with default values if neccessary. + # Called from the constructor and after every settings change. + def apply_settings(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBWorker, self).apply_settings() + if not "serial" in self.settings: self.settings.serial = None + if not "takeover" in self.settings: self.settings.takeover = False + if not "firmware" in self.settings or not self.settings.firmware: + self.settings.firmware = "modules/fpgamining/x6500/firmware/x6500.bit" + if not "jobinterval" in self.settings or not self.settings.jobinterval: self.settings.jobinterval = 60 + if not "pollinterval" in self.settings or not self.settings.pollinterval: self.settings.pollinterval = 0.1 + # We can't switch the device on the fly, so trigger a restart if they changed. + # self.serial is a cached copy of self.settings.serial. + if self.started and self.settings.serial != self.serial: self.async_restart() + # We need to inform the proxy about a poll interval change + if self.started and self.settings.pollinterval != self.pollinterval: self._notify_poll_interval_changed() + + + # Reset our state. Called both from the constructor and from self.start(). + def _reset(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBWorker, self)._reset() + # These need to be set here in order to make the equality check in apply_settings() happy, + # when it is run before starting the module for the first time. (It is called from the constructor.) + self.serial = None + self.pollinterval = None + self.stats.mhps = 0 + self.stats.errorrate = 0 + self.stats.temperature = None + + + # Start up the worker module. This is protected against multiple calls and concurrency by a wrapper. + def _start(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBWorker, self)._start() + # Cache the port number and baud rate, as we don't like those to change on the fly + self.serial = self.settings.serial + # Reset the shutdown flag for our threads + self.shutdown = False + # Start up the main thread, which handles pushing work to the device. + self.mainthread = Thread(None, self.main, self.settings.name + "_main") + self.mainthread.daemon = True + self.mainthread.start() + + + # Stut down the worker module. This is protected against multiple calls and concurrency by a wrapper. + def _stop(self): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBWorker, self)._stop() + # Set the shutdown flag for our threads, making them terminate ASAP. + self.shutdown = True + # Trigger the main thread's wakeup flag, to make it actually look at the shutdown flag. + with self.wakeup: self.wakeup.notify() + # Ping the proxy, otherwise the main thread might be blocked and can't wake up. + try: self._proxy_message("ping") + except: pass + # Wait for the main thread to terminate, which in turn kills the child workers. + self.mainthread.join(10) + + + # Report custom statistics. + def _get_statistics(self, stats, childstats): + # Let our superclass handle everything that isn't specific to this worker module + super(rphUSBWorker, self)._get_statistics(stats, childstats) + stats.errorrate = self.stats.errorrate + stats.temperature = self.stats.temperature + + + # Main thread entry point + # This thread is responsible for booting the individual FPGAs and spawning worker threads for them + def main(self): + # If we're currently shutting down, just die. If not, loop forever, + # to recover from possible errors caught by the huge try statement inside this loop. + # Count how often the except for that try was hit recently. This will be reset if + # there was no exception for at least 5 minutes since the last one. + tries = 0 + while not self.shutdown: + try: + # Record our starting timestamp, in order to back off if we repeatedly die + starttime = time.time() + self.dead = False + + # Check if we have a device serial number + if not self.serial: raise Exception("Device serial number not set!") + + # Try to start the board proxy + proxy_rxconn, self.txconn = Pipe(False) + self.rxconn, proxy_txconn = Pipe(False) + self.pollinterval = self.settings.pollinterval + self.proxy = rphUSBBoardProxy(proxy_rxconn, proxy_txconn, self.serial, + self.settings.takeover, self.settings.firmware, self.pollinterval) + self.proxy.daemon = True + self.proxy.start() + proxy_txconn.close() + self.response = None + self.response_queue = Queue() + + # Tell the board proxy to connect to the board + self._proxy_message("connect") + + while not self.shutdown: + data = self.rxconn.recv() + if self.dead: break + if data[0] == "log": self.core.log(self, "Proxy: %s" % data[1], data[2], data[3]) + elif data[0] == "ping": self._proxy_message("pong") + elif data[0] == "pong": pass + elif data[0] == "dying": raise Exception("Proxy died!") + elif data[0] == "response": self.response_queue.put(data[1:]) + elif data[0] == "started_up": self._notify_proxy_started_up(*data[1:]) + elif data[0] == "nonce_found": self._notify_nonce_found(*data[1:]) + elif data[0] == "speed_changed": self._notify_speed_changed(*data[1:]) + elif data[0] == "error_rate": self._notify_error_rate(*data[1:]) + elif data[0] == "keyspace_exhausted": self._notify_keyspace_exhausted(*data[1:]) + elif data[0] == "temperature_read": self._notify_temperature_read(*data[1:]) + else: raise Exception("Proxy sent unknown message: %s" % str(data)) + + + # If something went wrong... + except Exception as e: + # ...complain about it! + self.core.log(self, "%s\n" % traceback.format_exc(), 100, "rB") + finally: + with self.workloopwakeup: self.workloopwakeup.notify() + try: + for i in range(100): self.response_queue.put(None) + except: pass + try: self.workloopthread.join(2) + except: pass + try: self._proxy_message("shutdown") + except: pass + try: self.proxy.join(4) + except: pass + if not self.shutdown: + tries += 1 + if time.time() - starttime >= 300: tries = 0 + with self.wakeup: + if tries > 5: self.wakeup.wait(30) + else: self.wakeup.wait(1) + # Restart (handled by "while not self.shutdown:" loop above) + + + def _proxy_message(self, *args): + with self.lock: + self.txconn.send(args) + + + def _proxy_transaction(self, *args): + with self.transactionlock: + with self.lock: + self.txconn.send(args) + return self.response_queue.get() + + + def _notify_poll_interval_changed(self): + self.pollinterval = self.settings.pollinterval + try: self._proxy_message("set_pollinterval", self.pollinterval) + except: pass + + + def _notify_proxy_started_up(self): + # Assume a default job interval to make the core start fetching work for us. + # The actual hashrate will be measured (and this adjusted to the correct value) later. + self.jobs_per_second = 1. / self.settings.jobinterval + # This worker will only ever process one job at once. The work fetcher needs this information + # to estimate how many jobs might be required at once in the worst case (after a block was found). + self.parallel_jobs = 1 + # Start up the work loop thread, which handles pushing work to the device. + self.workloopthread = Thread(None, self._workloop, self.settings.name + "_workloop") + self.workloopthread.daemon = True + self.workloopthread.start() + + + def _notify_nonce_found(self, now, nonce): + # Snapshot the current jobs to avoid race conditions + oldjob = self.oldjob + newjob = self.job + # If there is no job, this must be a leftover from somewhere, e.g. previous invocation + # or reiterating the keyspace because we couldn't provide new work fast enough. + # In both cases we can't make any use of that nonce, so just discard it. + if not oldjob and not newjob: return + # Pass the nonce that we found to the work source, if there is one. + # Do this before calculating the hash rate as it is latency critical. + job = None + if newjob: + if newjob.nonce_found(nonce, oldjob): job = newjob + if not job and oldjob: + if oldjob.nonce_found(nonce): job = oldjob + + + def _notify_speed_changed(self, speed): + self.stats.mhps = speed / 1000000. + self.core.event(350, self, "speed", self.stats.mhps * 1000, "%f MH/s" % self.stats.mhps, worker = self) + self.core.log(self, "Running at %f MH/s\n" % self.stats.mhps, 300, "B") + # Calculate the time that the device will need to process 2**32 nonces. + # This is limited at 60 seconds in order to have some regular communication, + # even with very slow devices (and e.g. detect if the device was unplugged). + interval = min(60, 2**32 / speed) + # Add some safety margin and take user's interval setting (if present) into account. + self.jobinterval = min(self.settings.jobinterval, max(0.5, interval * 0.8 - 1)) + self.core.log(self, "Job interval: %f seconds\n" % self.jobinterval, 400, "B") + # Tell the MPBM core that our hash rate has changed, so that it can adjust its work buffer. + self.jobs_per_second = 1. / self.jobinterval + self.core.notify_speed_changed(self) + + + def _notify_error_rate(self, rate): + self.stats.errorrate = rate + + + def _notify_keyspace_exhausted(self): + with self.workloopwakeup: self.workloopwakeup.notify() + self.core.log(self, "Exhausted keyspace!\n", 200, "y") + + + def _notify_temperature_read(self, temp): + self.stats.temperature = temp + self.core.event(350, self, "temperature", temp * 1000, "%f \xc2\xb0C" % temp, worker = self) + + + def _send_job(self, job): + return self._proxy_transaction("send_job", job.data, job.midstate) + + + # This function should interrupt processing of the specified job if possible. + # This is necesary to avoid producing stale shares after a new block was found, + # or if a job expires for some other reason. If we don't know about the job, just ignore it. + # Never attempts to fetch a new job in here, always do that asynchronously! + # This needs to be very lightweight and fast. We don't care whether it's a + # graceful cancellation for this module because the work upload overhead is low. + def notify_canceled(self, job, graceful): + # Acquire the wakeup lock to make sure that nobody modifies job/nextjob while we're looking at them. + with self.workloopwakeup: + # If the currently being processed, or currently being uploaded job are affected, + # wake up the main thread so that it can request and upload a new job immediately. + if self.job == job: self.workloopwakeup.notify() + + + # Main thread entry point + # This thread is responsible for fetching work and pushing it to the device. + def _workloop(self): + try: + # Job that the device is currently working on, or that is currently being uploaded. + # This variable is used by BaseWorker to figure out the current work source for statistics. + self.job = None + # Job that was previously being procesed. Has been destroyed, but there might be some late nonces. + self.oldjob = None + + # We keep control of the wakeup lock at all times unless we're sleeping + self.workloopwakeup.acquire() + # Eat up leftover wakeups + self.workloopwakeup.wait(0) + + # Main loop, continues until something goes wrong or we're shutting down. + while not self.shutdown: + + # Fetch a job, add 2 seconds safety margin to the requested minimum expiration time. + # Blocks until one is available. Because of this we need to release the + # wakeup lock temporarily in order to avoid possible deadlocks. + self.workloopwakeup.release() + job = self.core.get_job(self, self.jobinterval + 2) + self.workloopwakeup.acquire() + + # If a new block was found while we were fetching that job, just discard it and get a new one. + if job.canceled: + job.destroy() + continue + + # Upload the job to the device + self._sendjob(job) + + # If the job was already caught by a long poll while we were uploading it, + # jump back to the beginning of the main loop in order to immediately fetch new work. + if self.job.canceled: continue + # Wait while the device is processing the job. If nonces are sent by the device, they + # will be processed by the listener thread. If the job gets canceled, we will be woken up. + self.workloopwakeup.wait(self.jobinterval) + + # If something went wrong... + except Exception as e: + # ...complain about it! + self.core.log(self, "%s\n" % traceback.format_exc(), 100, "rB") + finally: + # We're not doing productive work any more, update stats and destroy current job + self._jobend() + self.stats.mhps = 0 + # Make the proxy and its listener thread restart + self.dead = True + try: self.workloopwakeup.release() + except: pass + # Ping the proxy, otherwise the main thread might be blocked and can't wake up. + try: self._proxy_message("ping") + except: pass + + + # This function uploads a job to the device + def _sendjob(self, job): + # Move previous job to oldjob, and new one to job + self.oldjob = self.job + self.job = job + # Send it to the FPGA + start, now = self._send_job(job) + # Calculate how long the old job was running + if self.oldjob: + if self.oldjob.starttime: + self.oldjob.hashes_processed((now - self.oldjob.starttime) * self.stats.mhps * 1000000) + self.oldjob.destroy() + self.job.starttime = now + + + # This function needs to be called whenever the device terminates working on a job. + # It calculates how much work was actually done for the job and destroys it. + def _jobend(self, now = None): + # Hack to avoid a python bug, don't integrate this into the line above + if not now: now = time.time() + # Calculate how long the job was actually running and multiply that by the hash + # rate to get the number of hashes calculated for that job and update statistics. + if self.job != None: + if self.job.starttime: + self.job.hashes_processed((now - self.job.starttime) * self.stats.mhps * 1000000) + # Destroy the job, which is neccessary to actually account the calculated amount + # of work to the worker and work source, and to remove the job from cancelation lists. + self.oldjob = self.job + self.job.destroy() + self.job = None