Merge pull request #190 from UpstreamData/dev_iceriver

feature: Add iceriver support
This commit is contained in:
Brett Rowan
2024-09-01 09:53:39 -06:00
committed by GitHub
21 changed files with 388 additions and 0 deletions

View File

@@ -51,6 +51,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Mara Firmware Miners" return "Mara Firmware Miners"
case MinerTypes.BITAXE: case MinerTypes.BITAXE:
return "Stock Firmware BitAxe Miners" return "Stock Firmware BitAxe Miners"
case MinerTypes.ICERIVER:
return "Stock Firmware IceRiver Miners"
def create_url_str(mtype: str): def create_url_str(mtype: str):

View File

@@ -225,6 +225,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S19 Pro+ Hydro (BOS+)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19ProPlusHydro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (BOS+) ## T19 (BOS+)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19 ::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python handler: python
@@ -281,6 +288,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S19 Pro Hydro (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19ProHydro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (VNish) ## T19 (VNish)
::: pyasic.miners.antminer.vnish.X19.T19.VNishT19 ::: pyasic.miners.antminer.vnish.X19.T19.VNishT19
handler: python handler: python

View File

@@ -8,6 +8,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S21 Pro (Stock)
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## T21 (Stock) ## T21 (Stock)
::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21 ::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21
handler: python handler: python
@@ -36,6 +43,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S21 Pro (ePIC)
::: pyasic.miners.antminer.epic.X21.S21.ePICS21Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## T21 (ePIC) ## T21 (ePIC)
::: pyasic.miners.antminer.epic.X21.T21.ePICT21 ::: pyasic.miners.antminer.epic.X21.T21.ePICT21
handler: python handler: python

View File

@@ -0,0 +1,10 @@
# pyasic
## KSX Models
## KS2 (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS2.IceRiverKS2
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -89,6 +89,7 @@ details {
<summary>X21 Series:</summary> <summary>X21 Series:</summary>
<ul> <ul>
<li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li> <li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li>
<li><a href="../antminer/X21#s21-pro-stock">S21 Pro (Stock)</a></li>
<li><a href="../antminer/X21#t21-stock">T21 (Stock)</a></li> <li><a href="../antminer/X21#t21-stock">T21 (Stock)</a></li>
</ul> </ul>
</details> </details>
@@ -461,6 +462,7 @@ details {
<li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li> <li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li>
<li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li> <li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li>
<li><a href="../antminer/X19#s19-xp-bos_1">S19 XP (BOS+)</a></li> <li><a href="../antminer/X19#s19-xp-bos_1">S19 XP (BOS+)</a></li>
<li><a href="../antminer/X19#s19-pro_1-hydro-bos_1">S19 Pro+ Hydro (BOS+)</a></li>
<li><a href="../antminer/X19#t19-bos_1">T19 (BOS+)</a></li> <li><a href="../antminer/X19#t19-bos_1">T19 (BOS+)</a></li>
</ul> </ul>
</details> </details>
@@ -505,6 +507,7 @@ details {
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li> <li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li> <li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li>
<li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li> <li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-hydro-vnish">S19 Pro Hydro (VNish)</a></li>
<li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li> <li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li>
</ul> </ul>
</details> </details>
@@ -535,6 +538,7 @@ details {
<summary>X21 Series:</summary> <summary>X21 Series:</summary>
<ul> <ul>
<li><a href="../antminer/X21#s21-epic">S21 (ePIC)</a></li> <li><a href="../antminer/X21#s21-epic">S21 (ePIC)</a></li>
<li><a href="../antminer/X21#s21-pro-epic">S21 Pro (ePIC)</a></li>
<li><a href="../antminer/X21#t21-epic">T21 (ePIC)</a></li> <li><a href="../antminer/X21#t21-epic">T21 (ePIC)</a></li>
</ul> </ul>
</details> </details>
@@ -651,3 +655,14 @@ details {
</details> </details>
</ul> </ul>
</details> </details>
<details>
<summary>Stock Firmware IceRiver Miners:</summary>
<ul>
<details>
<summary>KSX Series:</summary>
<ul>
<li><a href="../iceriver/KSX#ks2-stock">KS2 (Stock)</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -339,6 +339,13 @@ class BitAxeModels(str, Enum):
return self.value return self.value
class IceRiverModels(str, Enum):
KS2 = "KS2"
def __str__(self):
return self.value
class MinerModel: class MinerModel:
ANTMINER = AntminerModels ANTMINER = AntminerModels
WHATSMINER = WhatsminerModels WHATSMINER = WhatsminerModels
@@ -348,3 +355,4 @@ class MinerModel:
AURADINE = AuradineModels AURADINE = AuradineModels
EPIC = ePICModels EPIC = ePICModels
BITAXE = BitAxeModels BITAXE = BitAxeModels
ICERIVER = IceRiverModels

View File

@@ -24,6 +24,7 @@ from .cgminer import CGMiner
from .epic import ePIC from .epic import ePIC
from .goldshell import GoldshellMiner from .goldshell import GoldshellMiner
from .hiveon import Hiveon from .hiveon import Hiveon
from .iceriver import IceRiver
from .innosilicon import Innosilicon from .innosilicon import Innosilicon
from .luxminer import LUXMiner from .luxminer import LUXMiner
from .marathon import MaraMiner from .marathon import MaraMiner

View File

@@ -0,0 +1,198 @@
from typing import List, Optional
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.device import MinerAlgo
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.miners.device.firmware import StockFirmware
from pyasic.web.iceriver import IceRiverWebAPI
ICERIVER_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_userpanel", "userpanel")],
),
}
)
class IceRiver(StockFirmware):
"""Handler for IceRiver miners"""
_web_cls = IceRiverWebAPI
web: IceRiverWebAPI
data_locations = ICERIVER_DATA_LOC
async def fault_light_off(self) -> bool:
try:
await self.web.locate(False)
except APIError:
return False
return True
async def fault_light_on(self) -> bool:
try:
await self.web.locate(True)
except APIError:
return False
return True
async def _get_fans(self, web_userpanel: dict = None) -> List[Fan]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
return [Fan(spd) for spd in web_userpanel["fans"]]
except (LookupError, ValueError, TypeError):
pass
async def _get_mac(self, web_userpanel: dict = None) -> Optional[str]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
return web_userpanel["mac"].upper().replace("-", ":")
except (LookupError, ValueError, TypeError):
pass
async def _get_hostname(self, web_userpanel: dict = None) -> Optional[str]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
return web_userpanel["host"]
except (LookupError, ValueError, TypeError):
pass
async def _get_hashrate(self, web_userpanel: dict = None) -> Optional[AlgoHashRate]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
base_unit = web_userpanel["unit"]
return AlgoHashRate.SHA256(
float(web_userpanel["rtpow"].replace(base_unit, "")),
unit=MinerAlgo.SHA256.unit.from_str(base_unit + "H"),
).into(MinerAlgo.SHA256.unit.default)
except (LookupError, ValueError, TypeError):
pass
async def _get_fault_light(self, web_userpanel: dict = None) -> bool:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
return web_userpanel["locate"]
except (LookupError, ValueError, TypeError):
pass
return False
async def _is_mining(self, web_userpanel: dict = None) -> Optional[bool]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
return web_userpanel["powstate"]
except (LookupError, ValueError, TypeError):
pass
async def _get_hashboards(self, web_userpanel: dict = None) -> List[HashBoard]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
hb_list = [
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if web_userpanel is not None:
try:
for board in web_userpanel["boards"]:
idx = board["no"] - 1
hb_list[idx].chip_temp = round(board["outtmp"])
hb_list[idx].temp = round(board["intmp"])
hb_list[idx].hashrate = AlgoHashRate.SHA256(
float(board["rtpow"].replace("G", "")), HashUnit.SHA256.GH
).into(self.algo.unit.default)
hb_list[idx].chips = board["chipnum"]
hb_list[idx].missing = False
except LookupError:
pass
return hb_list
async def _get_uptime(self, web_userpanel: dict = None) -> Optional[int]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
if web_userpanel is not None:
try:
runtime = web_userpanel["runtime"]
days, hours, minutes, seconds = runtime.split(":")
return (
(int(days) * 24 * 60 * 60)
+ (int(hours) * 60 * 60)
+ (int(minutes) * 60)
+ int(seconds)
)
except (LookupError, ValueError, TypeError):
pass

View File

@@ -48,3 +48,7 @@ class ePICMake(BaseMiner):
class BitAxeMake(BaseMiner): class BitAxeMake(BaseMiner):
make = MinerMake.BITAXE make = MinerMake.BITAXE
class IceRiverMake(BaseMiner):
make = MinerMake.BITAXE

View File

@@ -19,5 +19,6 @@ from .auradine import *
from .avalonminer import * from .avalonminer import *
from .epic import * from .epic import *
from .goldshell import * from .goldshell import *
from .iceriver import *
from .innosilicon import * from .innosilicon import *
from .whatsminer import * from .whatsminer import *

View File

@@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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.device.models import MinerModel
from pyasic.miners.device.makes import IceRiverMake
class KS2(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS2
expected_fans = 4

View File

@@ -0,0 +1 @@
from .KS2 import KS2

View File

@@ -0,0 +1 @@
from .KSX import *

View File

@@ -38,6 +38,7 @@ from pyasic.miners.bitaxe import *
from pyasic.miners.blockminer import * from pyasic.miners.blockminer import *
from pyasic.miners.device.makes import * from pyasic.miners.device.makes import *
from pyasic.miners.goldshell import * from pyasic.miners.goldshell import *
from pyasic.miners.iceriver import *
from pyasic.miners.innosilicon import * from pyasic.miners.innosilicon import *
from pyasic.miners.whatsminer import * from pyasic.miners.whatsminer import *
@@ -56,6 +57,7 @@ class MinerTypes(enum.Enum):
AURADINE = 10 AURADINE = 10
MARATHON = 11 MARATHON = 11
BITAXE = 12 BITAXE = 12
ICERIVER = 13
MINER_CLASSES = { MINER_CLASSES = {
@@ -451,6 +453,10 @@ MINER_CLASSES = {
"BM1366": BitAxeUltra, "BM1366": BitAxeUltra,
"BM1397": BitAxeMax, "BM1397": BitAxeMax,
}, },
MinerTypes.ICERIVER: {
None: type("IceRiverUnknown", (IceRiver, IceRiverMake), {}),
"KS2": IceRiverKS2,
},
} }
@@ -623,6 +629,8 @@ class MinerFactory:
return MinerTypes.INNOSILICON return MinerTypes.INNOSILICON
if "Miner UI" in web_text: if "Miner UI" in web_text:
return MinerTypes.AURADINE return MinerTypes.AURADINE
if "<TITLE>用户界面</TITLE>" in web_text:
return MinerTypes.ICERIVER
async def _get_miner_socket(self, ip: str) -> MinerTypes | None: async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
commands = ["version", "devdetails"] commands = ["version", "devdetails"]

View File

@@ -0,0 +1 @@
from .iceminer import *

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.iceriver import IceRiver
from pyasic.miners.device.models import KS2
class IceRiverKS2(IceRiver, KS2):
pass

View File

@@ -0,0 +1 @@
from .KS2 import IceRiverKS2

View File

@@ -0,0 +1 @@
from .KSX import *

View File

@@ -37,6 +37,7 @@ _settings = { # defaults
"default_auradine_web_password": "admin", "default_auradine_web_password": "admin",
"default_epic_web_password": "letmein", "default_epic_web_password": "letmein",
"default_hive_web_password": "admin", "default_hive_web_password": "admin",
"default_iceriver_web_password": "12345678",
"default_antminer_ssh_password": "miner", "default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root", "default_bosminer_ssh_password": "root",
} }

View File

@@ -19,5 +19,6 @@ from .base import BaseWebAPI
from .braiins_os import BOSerWebAPI, BOSMinerWebAPI from .braiins_os import BOSerWebAPI, BOSMinerWebAPI
from .epic import ePICWebAPI from .epic import ePICWebAPI
from .goldshell import GoldshellWebAPI from .goldshell import GoldshellWebAPI
from .iceriver import IceRiverWebAPI
from .innosilicon import InnosiliconWebAPI from .innosilicon import InnosiliconWebAPI
from .vnish import VNishWebAPI from .vnish import VNishWebAPI

77
pyasic/web/iceriver.py Normal file
View File

@@ -0,0 +1,77 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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 __future__ import annotations
import asyncio
import warnings
from typing import Any
import httpx
from pyasic import settings
from pyasic.errors import APIError
from pyasic.web.base import BaseWebAPI
class IceRiverWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.username = "admin"
self.pwd = settings.get("default_iceriver_web_password", "12345678")
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
tasks = {c: asyncio.create_task(getattr(self, c)()) for c in commands}
await asyncio.gather(*[t for t in tasks.values()])
return {t: tasks[t].result() for t in tasks}
async def send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
allow_warning: bool = True,
privileged: bool = False,
**parameters: Any,
) -> dict:
async with httpx.AsyncClient(transport=settings.transport()) as client:
try:
# auth
await client.post(
f"http://{self.ip}:{self.port}/user/loginpost",
params={"post": "6", "user": self.username, "pwd": self.pwd},
)
except httpx.HTTPError:
warnings.warn(f"Could not authenticate with miner web: {self}")
try:
resp = await client.post(
f"http://{self.ip}:{self.port}/user/{command}", params=parameters
)
if not resp.status_code == 200:
if not ignore_errors:
raise APIError(f"Command failed: {command}")
warnings.warn(f"Command failed: {command}")
return resp.json()
except httpx.HTTPError:
raise APIError(f"Command failed: {command}")
async def locate(self, enable: bool):
return await self.send_command(
"userpanel", post="5", locate="1" if enable else "0"
)
async def userpanel(self):
return await self.send_command("userpanel", post="4")