MiningOS Logo
Contribute To MiningOSAdd New Worker

Level 5 - Create a Brand/Model Specific Worker

Level 5 workers are concrete implementations that handle actual device communication for specific hardware brands or models.

B.1. Repository Naming Convention

Level 4 repositories follow strict naming patterns (see Naming Conventions).

miningos-wrk-{devicetype}-{brand}

Examples:

  • miningos-wrk-powermeter-abbABB power meters
  • miningos-wrk-powermeter-schneiderSchneider Electric power meters
  • miningos-wrk-miner-antminerBitmain Antminer hardware
  • miningos-wrk-miner-whatsminerMicroBT Whatsminer hardware
  • miningos-wrk-container-antspaceBitmain Antspace containers
  • miningos-wrk-sensor-senecaSeneca temperature sensors

For complete specifications of all supported devices, see Supported Devices.

No tpl in the name — Level 5 workers are concrete, not templates.

B.2. Worker File Naming Convention

Worker files follow a reverse naming pattern based on wtype:

workers/{brand}.rack.{devicetype}.wrk.js

Resolution Example:


// CLI: --wtype=wrk-miner-antminer
// Resolution: workers/antminer.rack.miner.wrk.js

For brands with multiple models requiring separate workers:

workers/{model}.rack.{devicetype}.wrk.js

Example (Schneider with multiple models):

miningos-wrk-powermeter-schneider/
├── workers/
│   ├── pm5340.rack.powermeter.wrk.js   # --wtype=wrk-powermeter-pm5340
│   └── p3u30.rack.powermeter.wrk.js    # --wtype=wrk-powermeter-p3u30

B.3. Directory Structure

A Level 5 implementation repository follows this structure:

miningos-wrk-{devicetype}-{brand}/
├── config/
│   ├── base.thing.json.example      # Brand-specific configuration
│   ├── common.json.example          # Common worker settings
│   └── facs/
│       ├── modbus.config.json.example  # Protocol-specific config (if needed)
│       ├── net.config.json.example     # Network/RPC configuration
│       └── store.config.json.example   # Storage configuration
├── mock/
│   └── mock-{brand}-device.js       # Mock device for testing
├── tests/
│   ├── cases/
│   │   └── {feature}.js             # Brand-specific test cases
│   ├── schema/
│   │   └── {feature}.js             # Brand-specific schema validators
│   ├── {brand}.spec.js              # Main test file
│   ├── executors.js                 # Test executors (may extend parent)
│   └── utils.js                     # Test utilities (extends parent)
├── workers/
│   ├── lib/
│   │   ├── {brand}.js               # Device library class
│   │   ├── constants.js             # Brand-specific constants
│   │   ├── registers.js             # Register maps (Modbus devices)
│   │   └── utils.js                 # Brand-specific utilities
│   └── {brand}.rack.{devicetype}.wrk.js  # Main worker implementation
├── LICENSE                           # Apache-2.0
├── README.md                         # Documentation
├── package.json                      # Dependencies
├── setup-config.sh                   # Configuration setup script
└── worker.js                         # Entry point

B.4. Package.json Structure

To create a package.json for a Level 5 worker:

  1. Open a text editor and create the following structure:
{
  "name": "miningos-wrk-{devicetype}-{brand}",
  "version": "0.0.1",
  "description": "MiningOS Worker {DeviceType} {Brand}",
  "author": {
    "name": "Your Name",
    "email": "your.email@example.com"
  },
  "maintainers": [
    {
      "name": "Your Name",
      "email": "your.email@example.com"
    }
  ],
  "keywords": ["miningos", "bitcoin", "mining", "{brand}"],
  "scripts": {
    "test": "brittle ./tests/**/*.spec.js",
    "lint": "standard",
    "lint:fix": "standard --fix"
  },
  "dependencies": {
    "async": "3.2.6",
    "bfx-svc-boot-js": "git+https://github.com/bitfinexcom/bfx-svc-boot-js.git",
    "miningos-tpl-wrk-{devicetype}": "git+https://github.com/tetherto/miningos-tpl-wrk-{devicetype}.git",
    "svc-facs-modbus": "git+https://github.com/tetherto/svc-facs-modbus.git"
  },
  "devDependencies": {
    "brittle": "3.16.3",
    "miningos-mock-control-service": "git+https://github.com/tetherto/miningos-mock-control-service.git"
  },
  "engine": {
    "node": ">=16.0"
  },
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/tetherto/miningos-wrk-{devicetype}-{brand}.git"
  }
}
  1. Replace \{devicetype\} and \{brand\} with your specific values.

  2. Update the author and maintainers fields.

  3. Save the file as package.json.

The dependency is the Level 4 template (miningos-tpl-wrk-{devicetype}), not the Level 3 thing worker.

B.5. Main Worker Implementation

To create the main worker file:

  1. Create workers/\{brand\}.rack.\{devicetype\}.wrk.js.

  2. Add the following implementation, replacing placeholders:

'use strict'

const WrkRack = require('miningos-tpl-wrk-{devicetype}/workers/rack.{devicetype}.wrk')
const {Brand}Device = require('./lib/{brand}')

class Wrk{DeviceType}Rack extends WrkRack {
  /**
   * Initialize the brand-specific worker.
   * Add protocol facilities required for device communication.
   */
  init () {
    super.init()
    
    // Add protocol-specific facility (e.g., Modbus for industrial devices)
    this.setInitFacs([
      ['fac', 'svc-facs-modbus', '0', '0', {}, 0]
    ])
    
    // Brand-specific initialization (optional)
    this.brandDefaults = {
      timeout: 30000,
      retries: 3
    }
  }

  /**
   * Returns the full type identifier including brand.
   * Concatenates parent type with brand suffix.
   * @returns {string} Full device type (e.g., 'powermeter-dkcyon')
   */
  getThingType () {
    return super.getThingType() + '-{brand}'
  }

  /**
   * Returns brand-specific tags for device categorization.
   * @returns {string[]} Array of brand tag strings
   */
  getThingTags () {
    return ['{brand}']
  }

  /**
   * Establishes connection to the physical device.
   * Creates device controller instance and assigns to thg.ctrl.
   * @param {Object} thg - Thing object containing opts and metadata
   * @returns {number} 1 on success, 0 on failure
   */
  async connectThing (thg) {
    // Validate required connection options
    if (!thg.opts.address || !thg.opts.port) {
      return 0  // Signal connection failure
    }
    
    // Create device instance with protocol client
    const device = new {Brand}Device({
      ...thg.opts,
      getClient: this.modbus_0.getClient.bind(this.modbus_0),
      conf: this.conf.thing?.{devicetype} || {}
    })
    
    // Set up error handling
    device.on('error', (e) => {
      this.debugThingError(thg, e)
    })
    
    // Assign controller to thing
    thg.ctrl = device
    return 1  // Signal success
  }

  /**
   * Collects current device state snapshot.
   * Called periodically by the scheduling system.
   * @param {Object} thg - Thing object with active controller
   * @returns {Object} Snapshot containing stats and config
   */
  async collectThingSnap (thg) {
    return thg.ctrl.getSnap()
  }

  /**
   * Selects device information for API responses.
   * Returns connection details and device-specific metadata.
   * @param {Object} thg - Thing object (optional, may use this context)
   * @returns {Object} Device information object
   */
  selectThingInfo (thg) {
    const thing = thg || this
    return {
      address: thing.opts?.address,
      port: thing.opts?.port,
      unitId: thing.opts?.unitId,
      model: thing.opts?.model,
      firmware: thing.info?.firmware
    }
  }
}

module.exports = Wrk{DeviceType}Rack

B.6. Device Library Class

Create a device library class that handles protocol communication (workers/lib/\{brand\}.js):

'use strict'

const EventEmitter = require('events')

class {Brand}Device extends EventEmitter {
  /**
   * @param {Object} opts - Device options
   * @param {string} opts.address - Device IP address
   * @param {number} opts.port - Device port
   * @param {number} opts.unitId - Modbus unit ID (for Modbus devices)
   * @param {Function} opts.getClient - Protocol client factory
   * @param {Object} opts.conf - Device configuration
   */
  constructor (opts) {
    super()
    
    this.address = opts.address
    this.port = opts.port
    this.unitId = opts.unitId || 1
    this.getClient = opts.getClient
    this.conf = opts.conf || {}
    
    this.client = null
    this.connected = false
  }

  /**
   * Establishes connection to device.
   * @returns {Promise<boolean>} Connection success
   */
  async connect () {
    try {
      this.client = await this.getClient(this.address, this.port)
      this.connected = true
      return true
    } catch (err) {
      this.emit('error', err)
      return false
    }
  }

  /**
   * Collects complete device snapshot.
   * @returns {Promise<Object>} Snapshot with stats and config
   */
  async getSnap () {
    const stats = await this.getStats()
    const config = await this.getConfig()
    
    return {
      success: true,
      stats,
      config
    }
  }

  /**
   * Collects device statistics/metrics.
   * @returns {Promise<Object>} Device metrics
   */
  async getStats () {
    // Implement protocol-specific data collection
    // Example for power meter:
    return {
      power: 0,
      voltage: { L1: 0, L2: 0, L3: 0 },
      current: { L1: 0, L2: 0, L3: 0 },
      energy: 0,
      frequency: 0,
      powerFactor: 0
    }
  }

      /**
       * Collects device configuration.
       * @returns {Promise<Object>} Device configuration
       */
      async getConfig () {
        return {
          model: 'Unknown',
          serial: 'Unknown',
          firmware: 'Unknown'
        }
      }

      /**
       * Gracefully closes device connection.
       */
      async close () {
        if (this.client && typeof this.client.close === 'function') {
          await this.client.close()
        }
        this.connected = false
      }
    }

module.exports = {Brand}Device

B.7. Protocol Facilities

Level 5 workers typically require protocol-specific facilities. Add them in init():

Modbus TCP (Industrial Devices)

  1. In your worker's init() method:

    init () {
      super.init()
      this.setInitFacs([
        ['fac', 'svc-facs-modbus', '0', '0', {}, 0]
      ])
    }
    // Access via: this.modbus_0.getClient(address, port)

HTTP/REST API

  1. In your worker's init() method:

    init () {
      super.init()
      this.setInitFacs([
        ['fac', 'bfx-facs-http', '0', '0', {}, 0]
      ])
    }
    // Access via: this.http_0

WebSocket

  1. In your worker's init() method:

    init () {
      super.init()
      this.setInitFacs([
        ['fac', 'svc-facs-ws', '0', '0', {}, 0]
      ])
    }
    // Access via: this.ws_0

B.8. Configuration Templates

base.thing.json.example

  1. Create or edit config/base.thing.json.example:
{
  "thing": {
    "{devicetype}DefaultPort": 502,
    "{devicetype}": {
      "delay": 50,
      "timeout": 30000,
      "{brand}": {
        "model": "default",
        "retries": 3
      }
    }
  }
}

facs/modbus.config.json.example (if using Modbus)

  1. Create config/facs/modbus.config.json.example:
{
  "0": {
    "connectionTimeout": 10000,
    "requestTimeout": 5000,
    "maxConnections": 10
  }
}

B.9. Testing Infrastructure

Level 5 tests inherit from Level 4 and add brand-specific cases:

Level 5 tests inherit from Level 4 and add brand-specific cases. For complete testing guidelines, see Testing & Linting Guidelines.

tests/utils.js

To extend the parent test utilities:

  1. Create tests/utils.js:

    'use strict'
    
    const utils = require('miningos-tpl-wrk-{devicetype}/tests/utils')
    const path = require('path')
    
    // Extend parent test infrastructure
    utils.SCHEMA_PATHS.push(path.join(__dirname, 'schema'))
    utils.TEST_PATHS.push(path.join(__dirname, 'cases'))
    
    module.exports = utils

tests/{brand}.spec.js

To create the main test file:

  1. Create tests/\{brand\}.spec.js:

    'use strict'
    
    const utils = require('./utils')
    const executors = require('./executors')
    
    // Run inherited tests plus brand-specific tests
    utils.runTests(executors)

tests/executors.js

To extend parent executors:

  1. Create tests/executors.js:

    'use strict'
    
    const parentExecutors = require('miningos-tpl-wrk-{devicetype}/tests/executors')
    
    // Extend or override parent executors
    module.exports = {
      ...parentExecutors,
    
      // Brand-specific executor example
      get{Brand}StatusExecutor: async (ctx) => {
        const result = await ctx.rpc.call('getThing', { id: ctx.thingId })
        return result
      }
    }

tests/cases/{feature}.js

To add a test case:

  1. Create tests/cases/\{feature\}.js:

    'use strict'
    
    const path = require('path')
    const { getSchema } = require(path.join(process.cwd(), 'tests/utils'))
    const { getSnapExecutor, getStatsExecutor } = require('../executors')
    const defaults = getSchema()
    
    module.exports = () => ({
      getSnap: {
        stages: [
          {
            name: 'getSnap',
            executor: getSnapExecutor,
            validate: {
              type: 'schema',
              schema: {
                success: { type: 'boolean' },
                stats: defaults.stats_validate.schema.stats,
                config: defaults.config_validate.schema.config
              }
            }
          }
        ]
      }
    })

B.10. Worker Entry Point

worker.js

  1. Create the worker.js entry point:

    'use strict'
    
    require('bfx-svc-boot-js')({
      wtype: 'wrk-{devicetype}-{brand}',
      conf: null
    })

For multimodel repositories, create separate entry points:

worker-pm5340.js

  1. Create a model-specific entry point:

    'use strict'
    
    require('bfx-svc-boot-js')({
      wtype: 'wrk-powermeter-pm5340',
      conf: null
    })

B.11. Implementation Requirements Summary

Each Level 5 implementation must:

RequirementMethod or LocationPurpose
Add protocol facilityinit() with setInitFacs()Enable device communication
Extend type definitiongetThingType()Return super.getThingType() + '-\{brand\}'
Implement connectionconnectThing(thg)Create device controller, assign to thg.ctrl
Implement data collectioncollectThingSnap(thg)Return \{ success, stats, config \}
Define device infoselectThingInfo(thg)Return connection or device metadata
Add brand tagsgetThingTags()Return brand-specific tags

B.12. Level 5 Checklist

  • Repository created with correct naming (miningos-wrk-\{devicetype\}-\{brand\})
  • Worker file follows naming convention (\{brand\}.rack.\{devicetype\}.wrk.js)
  • package.json depends on Level 4 template
  • package.json includes required protocol facility
  • package.json includes maintainers array
  • init() adds protocol facility via setInitFacs()
  • getThingType() returns super.getThingType() + '-\{brand\}'
  • getThingTags() returns brand-specific tags
  • connectThing() implemented with proper error handling
  • collectThingSnap() returns \{ success, stats, config \}
  • selectThingInfo() returns device metadata
  • Device library class created in workers/lib/
  • Test infrastructure extends Level 4 tests
  • Mock device created for testing
  • Configuration examples provided
  • README.md documents brand-specific features
  • Apache-2.0 license file included

On this page