Playing with Python and cgminer RPC API

I recently received an ASIC miner, and I use the popular cgminer, an ASIC miner (written in C) for Bitcoin (and Litecoin).

Cgminer provides an RPC API that let you retrieve stats about your device(s), and I'm buidling an open source monitoring dashboard, RigsMonitoring writen in Python, accessing the API with Python is easy using sockets.

Enable cgminer API

To enable the API, you need to add --api-listen and --api-allow W:0/0.

sudo nohup ./cgminer -o stratum.bitcoin.cz:3333 -u myworker -p mypass --api-allow W:0/0 --api-listen&

Accessing the API with Python

A detailled documentation is available in the API-README file.

The API talks JSON, listens on port 4048 by default, and a basic request looks like {"command":"CMD","parameter":"PARAM"}.

In [14]:
import socket
import json

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 4028))

Make a request with the summary command, JSON encoded.

In [15]:
sock.send(json.dumps({'command': 'summary'}))
Out[15]:
22

Next, receive the response (alway JSON encoded):

In [16]:
resp = ''
while 1:
    buf = sock.recv(4096)
    if buf:
        resp += buf
    else:
        break
In [17]:
resp
Out[17]:
'{"STATUS":[{"STATUS":"S","When":1378733100,"Code":11,"Msg":"Summary","Description":"cgminer 3.3.4"}],"SUMMARY":[{"Elapsed":8061,"MHS av":5363.24,"Found Blocks":0,"Getworks":303,"Accepted":3379,"Rejected":16,"Hardware Errors":1,"Utility":25.15,"Discarded":604,"Stale":0,"Get Failures":0,"Local Work":12271,"Remote Failures":0,"Network Blocks":23,"Total MH":43233140.8015,"Work Utility":75.47,"Difficulty Accepted":10137.00000000,"Difficulty Rejected":48.00000000,"Difficulty Stale":0.00000000,"Best Share":8944,"Device Hardware%":0.0099,"Device Rejected%":0.4734,"Pool Rejected%":0.4713,"Pool Stale%":0.0000}],"id":1}\x00'

You can notice the null byte \x00 at the end of the response.

Finally, we shutdown the socket.

In [18]:
sock.shutdown(socket.SHUT_RDWR)
sock.close()

And decode the response (without the null byte):

In [20]:
json.loads(resp[:-1])
Out[20]:
{u'STATUS': [{u'Code': 11,
   u'Description': u'cgminer 3.3.4',
   u'Msg': u'Summary',
   u'STATUS': u'S',
   u'When': 1378733100}],
 u'SUMMARY': [{u'Accepted': 3379,
   u'Best Share': 8944,
   u'Device Hardware%': 0.0099,
   u'Device Rejected%': 0.4734,
   u'Difficulty Accepted': 10137.0,
   u'Difficulty Rejected': 48.0,
   u'Difficulty Stale': 0.0,
   u'Discarded': 604,
   u'Elapsed': 8061,
   u'Found Blocks': 0,
   u'Get Failures': 0,
   u'Getworks': 303,
   u'Hardware Errors': 1,
   u'Local Work': 12271,
   u'MHS av': 5363.24,
   u'Network Blocks': 23,
   u'Pool Rejected%': 0.4713,
   u'Pool Stale%': 0.0,
   u'Rejected': 16,
   u'Remote Failures': 0,
   u'Stale': 0,
   u'Total MH': 43233140.8015,
   u'Utility': 25.15,
   u'Work Utility': 75.47}],
 u'id': 1}

Cgminer API wrapper

Now that we successfully called the API, here is a basic wrapper, with argument support:

In [3]:
import socket
import json


class CgminerAPI(object):
    """ Cgminer RPC API wrapper. """
    def __init__(self, host='localhost', port=4028):
        self.data = {}
        self.host = host
        self.port = port

    def command(self, command, arg=None):
        """ Initialize a socket connection,
        send a command (a json encoded dict) and
        receive the response (and decode it).
        """
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        try:
            sock.connect((self.host, self.port))
            payload = {"command": command}
            if arg is not None:
                # Parameter must be converted to basestring (no int)
                payload.update({'parameter': unicode(arg)})

            sock.send(json.dumps(payload))
            received = self._receive(sock)
        finally:
            sock.shutdown(socket.SHUT_RDWR)
            sock.close()
        
        return json.loads(received[:-1])

    def _receive(self, sock, size=4096):
        msg = ''
        while 1:
            chunk = sock.recv(size)
            if chunk:
                msg += chunk
            else:
                break
        return msg

    def __getattr__(self, attr):
        def out(arg=None):
            return self.command(attr, arg)
        return out

Check that the wrapper is actually working, by calling the command method:

In [4]:
cgminer = CgminerAPI()
In [5]:
cgminer.command('summary')
Out[5]:
{u'STATUS': [{u'Code': 11,
   u'Description': u'cgminer 3.3.4',
   u'Msg': u'Summary',
   u'STATUS': u'S',
   u'When': 1378918507}],
 u'SUMMARY': [{u'Accepted': 69859,
   u'Best Share': 1498416,
   u'Device Hardware%': 0.0005,
   u'Device Rejected%': 0.3646,
   u'Difficulty Accepted': 209577.0,
   u'Difficulty Rejected': 768.0,
   u'Difficulty Stale': 0.0,
   u'Discarded': 12286,
   u'Elapsed': 193470,
   u'Found Blocks': 0,
   u'Get Failures': 2,
   u'Getworks': 6147,
   u'Hardware Errors': 1,
   u'Local Work': 259407,
   u'MHS av': 4676.09,
   u'Network Blocks': 371,
   u'Pool Rejected%': 0.3651,
   u'Pool Stale%': 0.0,
   u'Rejected': 256,
   u'Remote Failures': 1,
   u'Stale': 3,
   u'Total MH': 904683321.2956,
   u'Utility': 21.67,
   u'Work Utility': 65.32}],
 u'id': 1}

Have you noticed the __getattr__ magic method, it return the command so instead of calling the command method, we can call the method that correspond to the command we want, it allows us to:

In [6]:
cgminer.summary()
Out[6]:
{u'STATUS': [{u'Code': 11,
   u'Description': u'cgminer 3.3.4',
   u'Msg': u'Summary',
   u'STATUS': u'S',
   u'When': 1378918691}],
 u'SUMMARY': [{u'Accepted': 69925,
   u'Best Share': 1498416,
   u'Device Hardware%': 0.0005,
   u'Device Rejected%': 0.3671,
   u'Difficulty Accepted': 209775.0,
   u'Difficulty Rejected': 774.0,
   u'Difficulty Stale': 0.0,
   u'Discarded': 12302,
   u'Elapsed': 193655,
   u'Found Blocks': 0,
   u'Get Failures': 2,
   u'Getworks': 6155,
   u'Hardware Errors': 1,
   u'Local Work': 259678,
   u'MHS av': 4676.69,
   u'Network Blocks': 372,
   u'Pool Rejected%': 0.3676,
   u'Pool Stale%': 0.0,
   u'Rejected': 258,
   u'Remote Failures': 1,
   u'Stale': 3,
   u'Total MH': 905662573.8391,
   u'Utility': 21.66,
   u'Work Utility': 65.33}],
 u'id': 1}

And it even handles argument:

In [7]:
cgminer.asc(0)
Out[7]:
{u'ASC': [{u'ASC': 0,
   u'Accepted': 69928,
   u'Device Hardware%': 0.0005,
   u'Device Rejected%': 0.367,
   u'Diff1 Work': 210881,
   u'Difficulty Accepted': 209784.0,
   u'Difficulty Rejected': 774.0,
   u'Enabled': u'Y',
   u'Hardware Errors': 1,
   u'ID': 0,
   u'Last Share Difficulty': 3.0,
   u'Last Share Pool': 0,
   u'Last Share Time': 1378918702,
   u'Last Valid Work': 1378918705,
   u'MHS 5s': 5412.69,
   u'MHS av': 4676.78,
   u'Name': u'BAJ',
   u'No Device': False,
   u'Rejected': 258,
   u'Status': u'Alive',
   u'Temperature': 35.0,
   u'Total MH': 905752768.1523,
   u'Utility': 21.66}],
 u'STATUS': [{u'Code': 106,
   u'Description': u'cgminer 3.3.4',
   u'Msg': u'ASC0',
   u'STATUS': u'S',
   u'When': 1378918707}],
 u'id': 1}

Pycgminer

I released the code on pypi and you can check out the project on github: pycgminer.

$ pip install pycgminer
In [4]:
from pycgminer import CgminerAPI
    
cgminer = CgminerAPI()
print cgminer.devs()
{u'STATUS': [{u'STATUS': u'S', u'Msg': u'1 ASC(s) - ', u'Code': 9, u'When': 1379356749, u'Description': u'cgminer 3.3.4'}], u'DEVS': [{u'Difficulty Accepted': 755613.0, u'Temperature': 36.0, u'Difficulty Rejected': 3070.0, u'Status': u'Alive', u'Device Rejected%': 0.4048, u'Rejected': 942, u'ID': 0, u'ASC': 0, u'Hardware Errors': 7, u'Accepted': 231709, u'No Device': False, u'Last Share Pool': 0, u'Diff1 Work': 758437, u'Name': u'BAJ', u'Total MH': 3255572325.457, u'Enabled': u'Y', u'Device Hardware%': 0.0009, u'Last Valid Work': 1379356749, u'Last Share Time': 1379356749, u'MHS av': 5153.57, u'MHS 5s': 5380.7, u'Last Share Difficulty': 4.0, u'Utility': 22.01}], u'id': 1}

Don't hesitate to submit a pull request!

Feedback

Don't hesitate to let me know if you have any questions/suggestions !

You should follow me on Twitter

Share this article

Tip with Bitcoin

Tip me with Bitcoin and vote for this post!

1FKdaZ75Ck8Bfc3LgQ8cKA8W7B86fzZBe2

Leave a comment

© Thomas Sileo. Powered by Pelican and hosted by DigitalOcean.