Compare commits

..

12 Commits

Author SHA1 Message Date
b-rowan
c50d55e87c version: bump version number. 2024-02-07 20:16:26 -07:00
b-rowan
5e5516bfb3 bug: fix serial numbers for antminer. 2024-02-07 20:15:38 -07:00
UpstreamData
4b068c57c5 version: bump version number. 2024-02-07 11:17:29 -07:00
UpstreamData
203f199aec feature: add wmt.pyasic.org. 2024-02-07 11:09:27 -07:00
b-rowan
895f17aaf9 version: bump version number. 2024-02-03 00:36:44 -07:00
b-rowan
8a64ff3559 bug: swap to asyncio.read() in base RPC to try to handle possible missed messages. 2024-02-03 00:36:03 -07:00
UpstreamData
4c45d356c4 version: bump version number. 2024-02-02 10:07:08 -07:00
UpstreamData
4dec329f11 bug: Try to return something when checking vnish fw version. 2024-02-02 10:06:33 -07:00
UpstreamData
b563ed118e bug: fix vnish firmware version bug. 2024-02-02 10:05:34 -07:00
UpstreamData
75b2ec40b1 bug: fix ePIC config parsing to use hashrate tuning instead of power tuning. 2024-01-31 09:21:32 -07:00
b-rowan
d9adaf6667 version: bump version number. 2024-01-30 21:41:49 -07:00
b-rowan
9343308f41 feature: Add support for new whatsminers, and try to handle whatsminer errors when receiving data. 2024-01-30 21:41:10 -07:00
13 changed files with 155 additions and 54 deletions

View File

@@ -108,31 +108,31 @@ class MiningModeHPM(MinerConfigValue):
return {"mode": {"mode": "turbo"}} return {"mode": {"mode": "turbo"}}
class StandardPowerTuneAlgo(MinerConfigValue): class StandardTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard") mode: str = field(init=False, default="standard")
def as_epic(self) -> str: def as_epic(self) -> str:
return VOptPowerTuneAlgo().as_epic() return VOptAlgo().as_epic()
class VOptPowerTuneAlgo(MinerConfigValue): class VOptAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard") mode: str = field(init=False, default="standard")
def as_epic(self) -> str: def as_epic(self) -> str:
return "VoltageOptimizer" return "VoltageOptimizer"
class ChipTunePowerTuneAlgo(MinerConfigValue): class ChipTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard") mode: str = field(init=False, default="standard")
def as_epic(self) -> str: def as_epic(self) -> str:
return "ChipTune" return "ChipTune"
class PowerTunerAlgo(MinerConfigOption): class TunerAlgo(MinerConfigOption):
standard = StandardPowerTuneAlgo standard = StandardTuneAlgo
voltage_optimizer = VOptPowerTuneAlgo voltage_optimizer = VOptAlgo
chip_tune = ChipTunePowerTuneAlgo chip_tune = ChipTuneAlgo
@classmethod @classmethod
def default(cls): def default(cls):
@@ -143,7 +143,7 @@ class PowerTunerAlgo(MinerConfigOption):
class MiningModePowerTune(MinerConfigValue): class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning") mode: str = field(init=False, default="power_tuning")
power: int = None power: int = None
algo: PowerTunerAlgo = field(default_factory=PowerTunerAlgo.default) algo: TunerAlgo = field(default_factory=TunerAlgo.default)
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune": def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune":
@@ -183,14 +183,12 @@ class MiningModePowerTune(MinerConfigValue):
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}} return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
def as_epic(self) -> dict:
return {"ptune": {"algo": self.algo.as_epic(), "target": self.power}}
@dataclass @dataclass
class MiningModeHashrateTune(MinerConfigValue): class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning") mode: str = field(init=False, default="hashrate_tuning")
hashrate: int = None hashrate: int = None
algo: TunerAlgo = field(default_factory=TunerAlgo.default)
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune": def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune":
@@ -218,6 +216,9 @@ class MiningModeHashrateTune(MinerConfigValue):
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}} return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
def as_epic(self) -> dict:
return {"ptune": {"algo": self.algo.as_epic(), "target": self.hashrate}}
@dataclass @dataclass
class ManualBoardSettings(MinerConfigValue): class ManualBoardSettings(MinerConfigValue):
@@ -313,14 +314,14 @@ class MiningModeConfig(MinerConfigOption):
if tuner_running: if tuner_running:
algo_info = web_conf["PerpetualTune"]["Algorithm"] algo_info = web_conf["PerpetualTune"]["Algorithm"]
if algo_info.get("VoltageOptimizer") is not None: if algo_info.get("VoltageOptimizer") is not None:
return cls.power_tuning( return cls.hashrate_tuning(
power=algo_info["VoltageOptimizer"]["Target"], hashrate=algo_info["VoltageOptimizer"]["Target"],
algo=PowerTunerAlgo.voltage_optimizer, algo=TunerAlgo.voltage_optimizer,
) )
else: else:
return cls.power_tuning( return cls.hashrate_tuning(
power=algo_info["ChipTune"]["Target"], hashrate=algo_info["ChipTune"]["Target"],
algo=PowerTunerAlgo.chip_tune, algo=TunerAlgo.chip_tune,
) )
else: else:
return cls.normal() return cls.normal()

View File

@@ -206,7 +206,7 @@ class AntminerModern(BMMiner):
] ]
try: try:
rpc_stats = await self.rpc.send_command("stats", new_rpc=True) rpc_stats = await self.rpc.send_command("stats", new_api=True)
except APIError: except APIError:
return hashboards return hashboards

View File

@@ -205,13 +205,14 @@ class VNish(BMMiner):
if web_summary is None: if web_summary is None:
web_summary = await self.web.summary() web_summary = await self.web.summary()
fw_ver = None
if web_summary is not None: if web_summary is not None:
try: try:
fw_ver = web_summary["miner"]["miner_type"] fw_ver = web_summary["miner"]["miner_type"]
fw_ver = fw_ver.split("(Vnish ")[1].replace(")", "") fw_ver = fw_ver.split("(Vnish ")[1].replace(")", "")
return fw_ver return fw_ver
except KeyError: except LookupError:
pass return fw_ver
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig:
try: try:

View File

@@ -289,7 +289,9 @@ MINER_CLASSES = {
"M50S++VK30": BTMinerM50SPlusPlusVK30, "M50S++VK30": BTMinerM50SPlusPlusVK30,
"M53VH30": BTMinerM53VH30, "M53VH30": BTMinerM53VH30,
"M53SVH30": BTMinerM53SVH30, "M53SVH30": BTMinerM53SVH30,
"M53SVJ40": BTMinerM53SVJ40,
"M53S+VJ30": BTMinerM53SPlusVJ30, "M53S+VJ30": BTMinerM53SPlusVJ30,
"M53S++VK10": BTMinerM53SPlusPlusVK10,
"M56VH30": BTMinerM56VH30, "M56VH30": BTMinerM56VH30,
"M56SVH30": BTMinerM56SVH30, "M56SVH30": BTMinerM56SVH30,
"M56S+VJ30": BTMinerM56SPlusVJ30, "M56S+VJ30": BTMinerM56SPlusVJ30,
@@ -968,6 +970,7 @@ class MinerFactory:
miner_factory = MinerFactory() miner_factory = MinerFactory()
# abstracted version of get miner that is easier to access # abstracted version of get miner that is easier to access
async def get_miner(ip: ipaddress.ip_address | str) -> AnyMiner: async def get_miner(ip: ipaddress.ip_address | str) -> AnyMiner:
return await miner_factory.get_miner(ip) return await miner_factory.get_miner(ip)

View File

@@ -20,3 +20,8 @@ from pyasic.miners.makes import WhatsMinerMake
class M53SVH30(WhatsMinerMake): class M53SVH30(WhatsMinerMake):
raw_model = "M53S VH30" raw_model = "M53S VH30"
expected_fans = 0 expected_fans = 0
class M53SVJ40(WhatsMinerMake):
raw_model = "M53S VJ40"
expected_fans = 0

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.makes import WhatsMinerMake
class M53SPlusPlusVK10(WhatsMinerMake):
raw_model = "M53S++ VK10"
expected_fans = 0

View File

@@ -42,8 +42,9 @@ from .M50S import (
from .M50S_Plus import M50SPlusVH30, M50SPlusVH40, M50SPlusVJ30, M50SPlusVK20 from .M50S_Plus import M50SPlusVH30, M50SPlusVH40, M50SPlusVJ30, M50SPlusVK20
from .M50S_Plus_Plus import M50SPlusPlusVK10, M50SPlusPlusVK20, M50SPlusPlusVK30 from .M50S_Plus_Plus import M50SPlusPlusVK10, M50SPlusPlusVK20, M50SPlusPlusVK30
from .M53 import M53VH30 from .M53 import M53VH30
from .M53S import M53SVH30 from .M53S import M53SVH30, M53SVJ40
from .M53S_Plus import M53SPlusVJ30 from .M53S_Plus import M53SPlusVJ30
from .M53S_Plus_Plus import M53SPlusPlusVK10
from .M56 import M56VH30 from .M56 import M56VH30
from .M56S import M56SVH30 from .M56S import M56SVH30
from .M56S_Plus import M56SPlusVJ30 from .M56S_Plus import M56SPlusVJ30

View File

@@ -15,8 +15,12 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import M5X from pyasic.miners.backends import M5X
from pyasic.miners.models import M53SVH30 from pyasic.miners.models import M53SVH30, M53SVJ40
class BTMinerM53SVH30(M5X, M53SVH30): class BTMinerM53SVH30(M5X, M53SVH30):
pass pass
class BTMinerM53SVJ40(M5X, M53SVJ40):
pass

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import M5X
from pyasic.miners.models import M53SPlusPlusVK10
class BTMinerM53SPlusPlusVK10(M5X, M53SPlusPlusVK10):
pass

View File

@@ -51,8 +51,9 @@ from .M50S_Plus_Plus import (
BTMinerM50SPlusPlusVK30, BTMinerM50SPlusPlusVK30,
) )
from .M53 import BTMinerM53VH30 from .M53 import BTMinerM53VH30
from .M53S import BTMinerM53SVH30 from .M53S import BTMinerM53SVH30, BTMinerM53SVJ40
from .M53S_Plus import BTMinerM53SPlusVJ30 from .M53S_Plus import BTMinerM53SPlusVJ30
from .M53S_Plus_Plus import BTMinerM53SPlusPlusVK10
from .M56 import BTMinerM56VH30 from .M56 import BTMinerM56VH30
from .M56S import BTMinerM56SVH30 from .M56S import BTMinerM56SVH30
from .M56S_Plus import BTMinerM56SPlusVJ30 from .M56S_Plus import BTMinerM56SPlusVJ30

View File

@@ -78,6 +78,9 @@ class BaseMinerRPCAPI:
# send the command # send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8")) data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
if data is None:
raise APIError("No data returned from the API.")
if data == b"Socket connect failed: Connection refused\n": if data == b"Socket connect failed: Connection refused\n":
if not ignore_errors: if not ignore_errors:
raise APIError(data.decode("utf-8")) raise APIError(data.decode("utf-8"))
@@ -193,12 +196,15 @@ If you are sure you want to use this command please use API.send_command("{comma
async def _send_bytes( async def _send_bytes(
self, self,
data: bytes, data: bytes,
port: int = None,
timeout: int = 100, timeout: int = 100,
) -> bytes: ) -> bytes:
if port is None:
port = self.port
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending") logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try: try:
# get reader and writer streams # get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port) reader, writer = await asyncio.open_connection(str(self.ip), port)
# handle OSError 121 # handle OSError 121
except OSError as e: except OSError as e:
if e.errno == 121: if e.errno == 121:
@@ -208,39 +214,14 @@ If you are sure you want to use this command please use API.send_command("{comma
return b"{}" return b"{}"
# send the command # send the command
data_task = asyncio.create_task(self._read_bytes(reader, timeout=timeout))
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing") logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
writer.write(data) writer.write(data)
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining") logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain() await writer.drain()
try:
# TO address a situation where a whatsminer has an unknown PW -AND-
# Fix for stupid whatsminer bug, reboot/restart seem to not load properly in the loop
# have to receive, save the data, check if there is more data by reading with a short timeout
# append that data if there is more, and then onto the main loop.
# the password timeout might need to be longer than 1, but it seems to work for now.
ret_data = await asyncio.wait_for(reader.read(1), timeout=1)
except asyncio.TimeoutError:
return b"{}"
try:
ret_data += await asyncio.wait_for(reader.read(4096), timeout=timeout)
except ConnectionAbortedError:
return b"{}"
# loop to receive all the data await data_task
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving") ret_data = data_task.result()
try:
while True:
try:
d = await asyncio.wait_for(reader.read(4096), timeout=timeout)
if not d:
break
ret_data += d
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
# close the connection # close the connection
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing") logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
@@ -249,6 +230,19 @@ If you are sure you want to use this command please use API.send_command("{comma
return ret_data return ret_data
async def _read_bytes(self, reader: asyncio.StreamReader, timeout: int) -> bytes:
ret_data = b""
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
try:
ret_data = await asyncio.wait_for(reader.read(), timeout=timeout)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
return ret_data
@staticmethod @staticmethod
def _load_api_data(data: bytes) -> dict: def _load_api_data(data: bytes) -> dict:
# some json from the API returns with a null byte (\x00) on the end # some json from the API returns with a null byte (\x00) on the end

View File

@@ -24,6 +24,7 @@ import logging
import re import re
from typing import Literal, Union from typing import Literal, Union
import httpx
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt from passlib.handlers.md5_crypt import md5_crypt
@@ -240,6 +241,28 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
ignore_errors: bool = False, ignore_errors: bool = False,
timeout: int = 10, timeout: int = 10,
**kwargs, **kwargs,
) -> dict:
try:
return await self._send_privileged_command(
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
)
except APIError as e:
if not e.message == "can't access write cmd":
raise
try:
await self.open_api()
except Exception as e:
raise APIError("Failed to open whatsminer API.") from e
return await self._send_privileged_command(
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
)
async def _send_privileged_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
timeout: int = 10,
**kwargs,
) -> dict: ) -> dict:
logging.debug( logging.debug(
f"{self} - (Send Privileged Command) - {command} " + f"with args {kwargs}" f"{self} - (Send Privileged Command) - {command} " + f"with args {kwargs}"
@@ -321,6 +344,30 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}") logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}")
return self.token return self.token
async def open_api(self):
async with httpx.AsyncClient() as c:
stage1_req = (
await c.post(
"https://wmt.pyasic.org/v1/stage1",
json={"ip": self.ip},
follow_redirects=True,
)
).json()
stage1_res = binascii.hexlify(
await self._send_bytes(binascii.unhexlify(stage1_req), port=8889)
)
stage2_req = (
await c.post(
"https://wmt.pyasic.org/v1/stage2",
json={"ip": self.ip, "stage1_result": stage1_res.decode("utf-8")},
)
).json()
try:
await self._send_bytes(binascii.unhexlify(stage2_req), timeout=3, port=8889)
except asyncio.TimeoutError:
pass
return True
#### PRIVILEGED COMMANDS #### #### PRIVILEGED COMMANDS ####
# Please read the top of this file to learn # Please read the top of this file to learn
# how to configure the Whatsminer API to # how to configure the Whatsminer API to

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.50.6" version = "0.51.1"
description = "A simplified and standardized interface for Bitcoin ASICs." description = "A simplified and standardized interface for Bitcoin ASICs."
authors = ["UpstreamData <brett@upstreamdata.ca>"] authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic" repository = "https://github.com/UpstreamData/pyasic"