├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Screenshot 2024-10-13 at 15.02.31.png ├── assets ├── icons │ ├── icon512_maskable.png │ └── icon512_rounded.png └── js │ ├── dp100.js │ └── ui.js ├── index.html └── manifest.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | max_line_length = 120 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - codingjoe 3 | - scottbez1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingjoe/DP100-WebApp/aa436ff96a210b89b5f8b86eb218558670dd5dd6/.gitignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome to the DP100 WebApp project! We are happy that you are interested in contributing to this project. 4 | 5 | ## Architecture 6 | 7 | ### WebHID 8 | 9 | This project is based on the [WebHID](https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API) API. 10 | It enables you to connect to Bluetooth or USB, like our power supply, via the browser. 11 | 12 | ### Javascript & ESM 13 | 14 | Since we already rely on a browser environment, 15 | we use the [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) module system. 16 | All code is written in vanilla Javascript. 17 | 18 | ### Dependencies 19 | 20 | We use [μPlot](https://github.com/leeoniya/uPlot) for the graphing because it is lightweight and fast. 21 | Everything else is build via Web Components with the help of [Lit](https://lit.dev/). 22 | 23 | ## Development 24 | 25 | This project is based on the [WebHID](https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API) API. 26 | It is a work-in-progress and not feature-complete. The Modbus implementation has been reverse-engineered 27 | from the Windows library (`ATK-DP100DLL(x64)_2.0.dll`), which can be found as part of the official software. 28 | 29 | If you want to contribute to this project, you can clone this repository and open the `index.html` file in your browser. 30 | 31 | You will need to enable write mode on Linux, since most distributions default to read-only. 32 | You can find this and other useful tips in the [Chrome Dev Tips][dev-tips]. 33 | 34 | [dev-tips]: https://developer.chrome.com/docs/capabilities/hid#dev-tips -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Johannes Maron 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DP100 WebApp 2 | 3 | A browser interface for the DP100 digital power supply by Alientek. 4 | 5 | ![Screenshot 2024-10-13 at 15.02.31.png](Screenshot%202024-10-13%20at%2015.02.31.png) 6 | 7 | ## Features 8 | 9 | - 🌐 Connect to the DP100 using your browser (no installation required). 10 | - 🍎 Works on all platforms (Windows, macOS, Linux). 11 | - 📈 Monitor your power diagram of the voltage and current levels. 12 | - 📏 Comfortably set the voltage and current levels. 13 | - ❤️ Free, open source and build with love! 14 | 15 | ## Usage 16 | 17 | Grap you DP100 power supply and connect the DP100 via it's USB-A port to your computer. 18 | 19 | Now, visit [DP100 WebApp](https://johannes.maron.family/DP100-WebApp/) and you're good to go. 20 | 21 | _Note, not all browsers support WebHID yet and on Linux you might need to enable write mode first, 22 | see [Contributing Guide](CONTRIBUTING.md)._ 23 | 24 | ## Development & Contributing 25 | 26 | We welcome contributions to this project. Please read the [Contributing Guidelines](CONTRIBUTING.md) for more information. 27 | 28 | ## Credits 29 | 30 | Special thanks for [@scottbez1](https://github.com/scottbez1) for inspiring this project and being an overall camp. 31 | -------------------------------------------------------------------------------- /Screenshot 2024-10-13 at 15.02.31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingjoe/DP100-WebApp/aa436ff96a210b89b5f8b86eb218558670dd5dd6/Screenshot 2024-10-13 at 15.02.31.png -------------------------------------------------------------------------------- /assets/icons/icon512_maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingjoe/DP100-WebApp/aa436ff96a210b89b5f8b86eb218558670dd5dd6/assets/icons/icon512_maskable.png -------------------------------------------------------------------------------- /assets/icons/icon512_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingjoe/DP100-WebApp/aa436ff96a210b89b5f8b86eb218558670dd5dd6/assets/icons/icon512_rounded.png -------------------------------------------------------------------------------- /assets/js/dp100.js: -------------------------------------------------------------------------------- 1 | const vendorId = 11836, productId = 44801 // DP100's HID IDs 2 | const deviceAddr = 251 // DP100's device address 3 | 4 | /** 5 | * Calculate the buffers CRC-16/MODBUS checksum. 6 | * 7 | * @param {ArrayBuffer} buffer - The buffer to calculate the CRC16 for. 8 | * @returns {Number} - The CRC16 checksum. 9 | */ 10 | export function crc16 (buffer) { 11 | let crc = 0xFFFF 12 | 13 | for (const byte of new Uint8Array(buffer)) { 14 | crc = crc ^ byte 15 | 16 | for (let j = 0; j < 8; j++) { 17 | const odd = crc & 0x0001 18 | crc = crc >> 1 19 | if (odd) { 20 | crc = crc ^ 0xA001 21 | } 22 | } 23 | } 24 | 25 | return crc 26 | } 27 | 28 | /** DP100 Modbus Function IDs */ 29 | const FUNCTIONS = Object.freeze({ 30 | DEVICE_INFO: 0x10, // 16 31 | FIRM_INFO: 0x11, // 17 32 | START_TRANS: 0x12, // 18 33 | DATA_TRANS: 0x13, // 19 34 | END_TRANS: 0x14, // 20 35 | DEV_UPGRADE: 0x15, // 21 36 | BASIC_INFO: 0x30, // 48 37 | BASIC_SET: 0x35, // 53 38 | SYSTEM_INFO: 0x40, // 64 39 | SYSTEM_SET: 0x45, // 69 40 | SCAN_OUT: 0x50, // 80 41 | SERIAL_OUT: 0x55, // 85 42 | DISCONNECT: 0x80, // 128 43 | NONE: 0xFF // 255 44 | }) 45 | 46 | const MAGIC_BYTES = Object.freeze({ 47 | OUTPUT: 0x20, // 32 48 | SETTING: 0x40, // 64 49 | READ: 0x80 // 128 50 | }) 51 | 52 | /** DP100 device class. 53 | * 54 | * This class is used to interact with the DP100 power supply. 55 | * @example 56 | * 57 | * class MyPSU extends DP100() { 58 | * receiveBasicInfo ({vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt}) { 59 | * console.info('Input Voltage:', vIn, 'V') 60 | * console.info('Output Voltage:', vOut, 'V') 61 | * console.info('Output Current:', iOut, 'A') 62 | * console.info('Max Output Voltage:', voMax, 'V') 63 | * console.info('Temperature 1:', temp1, '°C') 64 | * console.info('Temperature 2:', temp2, '°C') 65 | * console.info('DC 5V:', dc5V, 'V') 66 | * console.info('Output Mode:', outMode) 67 | * console.info('Work State:', workSt) 68 | * } 69 | * } 70 | * 71 | * const psu = new MyPSU() 72 | * await psu.connect() 73 | * 74 | * @param {*} Base - The base class to extend. 75 | * @mixin 76 | * @returns {Base} The new class. 77 | */ 78 | export function DP100 (Base) { 79 | return class extends Base { 80 | 81 | settingsQueue = [] 82 | refreshRate = 10 // 10ms (100Hz) 83 | 84 | /** Connect to the DP100 device. */ 85 | async connect () { 86 | [this.device] = await navigator.hid.requestDevice({ 87 | filters: [{ vendorId, productId }] 88 | }) 89 | await this.device.open() 90 | this.device.addEventListener('inputreport', this.inputReportHandler.bind(this)) 91 | this.device.addEventListener('disconnect', () => { 92 | console.warn('Device disconnected') 93 | this.device = null 94 | clearInterval(this.updateLoop) 95 | }) 96 | this.sendReport(FUNCTIONS.SYSTEM_INFO) 97 | this.sendReport(FUNCTIONS.DEVICE_INFO) 98 | this.getBasicSettings().then(() => { 99 | this.updateLoop = setInterval(() => { 100 | this.sendReport(FUNCTIONS.BASIC_INFO) 101 | }, this.refreshRate) 102 | }) 103 | } 104 | 105 | /** 106 | * Send a report to the DP100. 107 | * 108 | * @param {Number} functionId -- The function to call on the DP100. 109 | * @param {Uint8Array} content -- The data to send to the DP100. 110 | * @param {Number} sequence -- The sequence number for the report. 111 | * @returns {Promise} -- A promise that resolves when the report is sent. 112 | */ 113 | async sendReport (functionId, content = null, sequence = null) { 114 | content = content || new Uint8Array([0]) 115 | const header = [deviceAddr, functionId, sequence, // sequence, unused if there is no content 116 | content.length, ...content, 0, // checksum 117 | 0 // checksum 118 | ] 119 | if (sequence === null) { 120 | header.splice(2, 1) 121 | } 122 | const report = new Uint8Array(header) 123 | const reportView = new DataView(report.buffer, report.byteOffset, report.byteLength) 124 | const checksum = crc16(report.buffer.slice(0, report.length - 2)) 125 | reportView.setUint16(report.length - 2, checksum, true) 126 | console.debug('device.sendReport', reportView) 127 | return await this.device.sendReport(0, report) 128 | } 129 | 130 | async getBasicSettings () { 131 | await this.sendReport(FUNCTIONS.BASIC_SET, new Uint8Array([MAGIC_BYTES.READ]), 0) 132 | } 133 | 134 | async setBasicOutput ({ state, vo_set, io_set }) { 135 | if (this.settings === undefined) { 136 | throw new Error('Settings not loaded') 137 | } 138 | console.info('setBasicOutput', { state, vo_set, io_set }) 139 | const basicSet = Object.assign({}, this.settings, Object.fromEntries(Object.entries({ 140 | state, vo_set, io_set 141 | }).filter(([k, v]) => v !== undefined))) 142 | this.settingsQueue.push(basicSet) 143 | const out = new Uint8Array(10) 144 | const outDv = new DataView(out.buffer, out.byteOffset, out.length) 145 | outDv.setUint8(0, MAGIC_BYTES.OUTPUT) 146 | outDv.setUint8(1, basicSet.state) 147 | outDv.setUint16(2, basicSet.vo_set * 1000, true) 148 | outDv.setUint16(4, basicSet.io_set * 1000, true) 149 | await this.sendReport(FUNCTIONS.BASIC_SET, out, 0) 150 | } 151 | 152 | async setBasicSettings ({ ovp_set, ocp_set }) { 153 | if (this.settings === undefined) { 154 | throw new Error('Settings not loaded') 155 | } 156 | console.info('setBasicSettings', { ovp_set, ocp_set }) 157 | const basicSet = Object.assign({}, this.settings, Object.fromEntries(Object.entries({ 158 | ovp_set, ocp_set 159 | }).filter(([k, v]) => v !== undefined))) 160 | this.settingsQueue.push(basicSet) 161 | const out = new Uint8Array(10) 162 | const outDv = new DataView(out.buffer, out.byteOffset, out.length) 163 | outDv.setUint8(0, MAGIC_BYTES.SETTING) 164 | outDv.setUint16(6, basicSet.ovp_set * 1000, true) 165 | outDv.setUint16(8, basicSet.ocp_set * 1000, true) 166 | await this.sendReport(FUNCTIONS.BASIC_SET, out, 0) 167 | } 168 | 169 | /** Handle input reports from the DP100 170 | * @param {HIDInputReportEvent} event 171 | */ 172 | inputReportHandler (event) { 173 | console.debug('device.inputreport', event) 174 | const data = event.data 175 | const headerLength = 4 176 | const header = { 177 | deviceAddr: data.getUint8(0), 178 | functionType: event.data.getUint8(1), 179 | sequence: event.data.getUint8(2), 180 | contentLength: event.data.getUint8(3), 181 | } 182 | const contentView = new DataView(data.buffer.slice(headerLength, headerLength + header.contentLength)) 183 | const checksum = data.getUint16(headerLength + header.contentLength, true) 184 | const computedChecksum = crc16(data.buffer.slice(0, headerLength + header.contentLength)) 185 | if (computedChecksum !== checksum) { 186 | console.error('Checksum Failed', { 187 | expected: computedChecksum.toString(16), received: checksum.toString(16) 188 | }) 189 | return 190 | } 191 | switch (header.functionType) { 192 | case FUNCTIONS.BASIC_INFO: 193 | this.receiveBasicInfo({ 194 | vIn: contentView.getUint16(0, true) / 1000, 195 | vOut: contentView.getUint16(2, true) / 1000, 196 | iOut: contentView.getUint16(4, true) / 1000, 197 | voMax: contentView.getUint16(6, true) / 1000, 198 | temp1: contentView.getUint16(8, true) / 10, 199 | temp2: contentView.getUint16(10, true) / 10, 200 | dc5V: contentView.getUint16(12, true) / 1000, 201 | outMode: contentView.getUint8(14), 202 | workSt: contentView.getUint8(15) 203 | }) 204 | break 205 | case FUNCTIONS.BASIC_SET: 206 | if (contentView.byteLength === 1 && contentView.getUint8(0)) { 207 | this.settings = this.settingsQueue.pop() 208 | break 209 | } 210 | this.receiveBasicSettings({ 211 | ack: contentView.getUint8(0), 212 | state: contentView.getUint8(1), 213 | vo_set: contentView.getUint16(2, true) / 1000, 214 | io_set: contentView.getUint16(4, true) / 1000, 215 | ovp_set: contentView.getUint16(6, true) / 1000, 216 | ocp_set: contentView.getUint16(8, true) / 1000, 217 | }) 218 | break 219 | case FUNCTIONS.SYSTEM_INFO: 220 | this.receiveSystemInfo({ 221 | otp: contentView.getUint16(0, true), 222 | opp: contentView.getUint16(2, true) / 10.0, 223 | backlight: contentView.getUint8(4), 224 | volume: contentView.getUint8(5), 225 | reverse_protection: contentView.getUint8(6), 226 | audio_out: contentView.getUint8(7), 227 | }) 228 | break 229 | case FUNCTIONS.DEVICE_INFO: 230 | console.debug({ 231 | deviceName: String.fromCharCode(...new Uint8Array(contentView.buffer.slice(0, 15))), 232 | hardwareVersion: contentView.getUint16(16, true) / 10, 233 | firmwareVersion: contentView.getUint16(18, true) / 10, 234 | bootVersion: contentView.getUint16(20, true), 235 | runVersion: contentView.getUint16(22, true), 236 | serialNumber: new Uint8Array(contentView.buffer.slice(24, 24 + 11)).join(''), 237 | year: contentView.getUint16(36, true), 238 | month: contentView.getUint8(38), 239 | day: contentView.getUint8(39), 240 | }) 241 | break 242 | default: 243 | console.warn('Unhandled function', header.functionType, contentView) 244 | } 245 | } 246 | 247 | /** Handle basic info from the DP100 248 | * @param {Object} basicInfo 249 | * @param {Number} basicInfo.vIn - Input voltage in V. 250 | * @param {Number} basicInfo.vOut - Output voltage in V. 251 | * @param {Number} basicInfo.iOut - Output current in A. 252 | * @param {Number} basicInfo.voMax - Max output voltage in V. 253 | * @param {Number} basicInfo.temp1 - Temperature 1 in °C. 254 | * @param {Number} basicInfo.temp2 - Temperature 2 in °C. 255 | * @param {Number} basicInfo.dc5V - 5V rail in V. 256 | * @param {Number} basicInfo.outMode - Output mode. 257 | * @param {Number} basicInfo.workSt - Work state. 258 | */ 259 | receiveBasicInfo ({ vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt }) { 260 | console.debug('receiveBasicInfo', { vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt }) 261 | this.info = { vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt } 262 | } 263 | 264 | /** Handle basic settings from the DP100 265 | * @param {Object} basicSettings 266 | * @param {boolean} basicSettings.ack - Acknowledgement. 267 | * @param {boolean} basicSettings.state - Setting state. 268 | * @param {Number} basicSettings.vo_set - Output voltage setting in V. 269 | * @param {Number} basicSettings.io_set - Output current setting in A. 270 | * @param {Number} basicSettings.ovp_set - Over-voltage protection setting in V. 271 | * @param {Number} basicSettings.ocp_set - Over-current protection setting in A. 272 | */ 273 | receiveBasicSettings ({ ack, state, vo_set, io_set, ovp_set, ocp_set }) { 274 | console.info('receiveBasicSettings', { ack, state, vo_set, io_set, ovp_set, ocp_set }) 275 | this.settings = { state, vo_set, io_set, ovp_set, ocp_set } 276 | } 277 | 278 | /** Handle system info from the DP100 279 | * @param {Object} system 280 | * @param {Number} system.backlight - Backlight setting between 0 and 4. 281 | * @param {Number} system.volume - Volume setting between 0 and 4. 282 | * @param {Number} system.opp - Over-power protection setting in W. 283 | * @param {Number} system.otp - Over-temperature protection setting in C (range: 50 – 80). 284 | * @param {boolean} system.reverse_protection - Reverse protection setting. 285 | * @param {boolean} system.audio_out - Audio output setting. 286 | */ 287 | receiveSystemInfo ({ backlight, volume, opp, otp, reverse_protection, audio_out }) { 288 | console.info('receiveSystemInfo', { backlight, volume, opp, otp, reverse_protection, audio_out }) 289 | this.system = { backlight, volume, opp, otp, reverse_protection, audio_out } 290 | } 291 | 292 | } 293 | } -------------------------------------------------------------------------------- /assets/js/ui.js: -------------------------------------------------------------------------------- 1 | import uplot from 'uplot' 2 | import { LitElement, html, css } from 'lit' 3 | import { DP100 } from './dp100.js' 4 | 5 | const dark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches 6 | 7 | const grapOptions = { 8 | id: 'uv-graph', 9 | series: [ 10 | { 11 | label: 'Time', 12 | value: (self, rawValue) => rawValue === null ? ' N/A' : new Date(rawValue * 1000).toLocaleTimeString(), 13 | }, 14 | { 15 | show: true, 16 | spanGaps: true, 17 | label: 'Voltage', 18 | value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'V', 19 | scale: 'V', 20 | stroke: 'rgb(250, 200, 0)', 21 | width: 2, 22 | }, { 23 | show: true, 24 | spanGaps: true, 25 | label: 'Current', 26 | value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'A', 27 | scale: 'A', 28 | stroke: 'green', 29 | width: 2, 30 | }, { 31 | show: true, 32 | spanGaps: true, 33 | label: 'Power', 34 | value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'W', 35 | scale: 'W', 36 | fill: 'rgba(200, 0, 200, 0.3)', 37 | width: 0, 38 | } 39 | ], 40 | axes: [ 41 | { 42 | show: false 43 | }, 44 | { 45 | scale: 'V', 46 | label: 'Voltage (V)', 47 | value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'V', 48 | grid: { show: false }, 49 | stroke: () => dark ? 'white' : 'black', 50 | ticks: { 51 | stroke: () => dark ? 'white' : 'black', 52 | }, 53 | }, 54 | { 55 | scale: 'A', 56 | label: 'Current (A)', 57 | value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'A', 58 | side: 1, 59 | grid: { show: false }, 60 | stroke: () => dark ? 'white' : 'black', 61 | ticks: { 62 | stroke: () => dark ? 'white' : 'black', 63 | }, 64 | }, 65 | {}, 66 | ], 67 | scales: { 68 | 'x': {}, 69 | 'V': { 70 | auto: false, 71 | range: [0, 30], 72 | }, 73 | 'A': { 74 | auto: false, 75 | range: [0, 5], 76 | }, 77 | 'W': { 78 | auto: false, 79 | range: [0, 100], 80 | } 81 | }, 82 | } 83 | 84 | export class DP100Element extends DP100(LitElement) { 85 | tHistory = [] 86 | vHistory = [] 87 | iHistory = [] 88 | pHistory = [] 89 | 90 | static properties = { 91 | device: { type: Object, attribute: false, reflect: true }, 92 | settings: { type: Object, attribute: false, reflect: true }, 93 | info: { type: Object, attribute: false, reflect: true }, 94 | vMax: { type: Number, attribute: false, reflect: true }, 95 | iMax: { type: Number, attribute: false, reflect: true }, 96 | pMax: { type: Number, attribute: false, reflect: true }, 97 | } 98 | static styles = css` 99 | :host { 100 | display: grid; 101 | grid-template: 102 | "graph graph graph vOut" 2fr 103 | "graph graph graph iOut" 2fr 104 | "graph graph graph pOut" 1fr 105 | "controls controls controls controls" 120px / 1fr 1fr 1fr minmax(42vh, max-content); 106 | height: 100vh; 107 | overflow: hidden; 108 | } 109 | 110 | * { 111 | font-family: monospace; 112 | } 113 | 114 | .value { 115 | font-size: 4vh; 116 | } 117 | 118 | .group { 119 | display: grid; 120 | grid-template: 121 | "label value-1" 122 | "label value-2" / min-content auto; 123 | 124 | .label { 125 | grid-area: label; 126 | font-weight: bold; 127 | font-size: min(4vw, 4em); 128 | margin: auto 0; 129 | 130 | sub { 131 | font-size: 2rem; 132 | } 133 | } 134 | 135 | .value-1, .value-2 { 136 | grid-area: auto; 137 | font-size: 2em; 138 | } 139 | 140 | .value-1 { 141 | margin: auto 0 0.8em; 142 | 143 | input { 144 | max-width: 4em; 145 | } 146 | } 147 | 148 | .value-2 { 149 | margin: 0 0 auto; 150 | } 151 | } 152 | 153 | .group--big { 154 | grid-template: 155 | "label value-1" 156 | "value-2 value-2" / min-content auto; 157 | 158 | .value-1 { 159 | margin-left: -0.5em; 160 | } 161 | 162 | .value-2 { 163 | grid-column: 1 / 3; 164 | line-height: 0; 165 | 166 | input { 167 | width: 100%; 168 | } 169 | } 170 | } 171 | 172 | #graph { 173 | grid-area: graph; 174 | border: thick solid CanvasText; 175 | } 176 | 177 | #vOut, #iOut, #pOut { 178 | font-size: 2em; 179 | display: flex; 180 | flex-direction: column; 181 | justify-content: space-evenly; 182 | padding: 0 0.5em; 183 | } 184 | 185 | #vOut { 186 | grid-area: vOut; 187 | background: repeating-linear-gradient( 188 | 135deg, 189 | rgb(250 200 0 / 33%), 190 | rgb(250 200 0 / 67%) 191 | ); 192 | border: thick solid rgb(250 200 0 / 100%); 193 | } 194 | 195 | #iOut { 196 | grid-area: iOut; 197 | background: repeating-linear-gradient( 198 | 135deg, 199 | rgb(0 200 0 / 33%), 200 | rgb(0 200 0 / 67%) 201 | ); 202 | border: thick solid rgb(0 200 0 / 100%); 203 | } 204 | 205 | #pOut { 206 | grid-area: pOut; 207 | background: repeating-linear-gradient( 208 | 135deg, 209 | rgb(200 0 200 / 33%), 210 | rgb(200 0 200 / 67%) 211 | ); 212 | border: thick solid rgb(200 0 200 / 100%); 213 | font-size: 2em; 214 | } 215 | 216 | #controls { 217 | grid-area: controls; 218 | display: flex; 219 | flex-direction: row; 220 | justify-content: space-between; 221 | gap: 1em; 222 | } 223 | 224 | #mode { 225 | flex: 0 0 32vw; 226 | } 227 | 228 | #opp, #vInMax, #info { 229 | padding: 0.5em; 230 | gap: 0.5em; 231 | } 232 | 233 | input:invalid { 234 | border: medium dashed red; 235 | } 236 | 237 | input[type=number] { 238 | border: 0; 239 | background: none; 240 | font-size: 1em; 241 | max-width: 4em; 242 | font-family: monospace; 243 | border-bottom: medium dotted FieldText; 244 | background-color: color-mix(in srgb, ButtonFace, transparent 50%); 245 | } 246 | 247 | button { 248 | font-size: min(5vw, 5em); 249 | font-weight: bold; 250 | width: 100%; 251 | height: 100%; 252 | border: none; 253 | background-color: #efefef; 254 | color: black 255 | } 256 | ` 257 | 258 | constructor () { 259 | super() 260 | this.vMax = 0 261 | this.iMax = 0 262 | this.pMax = 0 263 | this.energy = 0 264 | this.timer = Date.now() 265 | } 266 | 267 | render () { 268 | return html` 269 | 270 |
271 |
272 |
273 |
274 | Vset 275 |
276 |
277 | V 280 |
281 |
282 | 285 |
286 |
287 |
288 | Vout 289 | ${this.info?.vOut.toLocaleString(undefined, { 290 | minimumFractionDigits: 3, 291 | minimumIntegerDigits: 2, 292 | useGrouping: false 293 | })} 294 | V 295 |
296 |
297 | Vmax 298 | ${(this.vMax).toLocaleString(undefined, { 299 | minimumFractionDigits: 3, 300 | minimumIntegerDigits: 2, 301 | useGrouping: false 302 | })} 303 | V 304 |
305 |
306 |
307 |
308 |
309 | Iset 310 |
311 |
312 | A 315 |
316 |
317 | 320 |
321 |
322 |
323 | Iout 324 | ${this.info?.iOut.toLocaleString(undefined, { 325 | minimumFractionDigits: 3, 326 | minimumIntegerDigits: 2, 327 | useGrouping: false 328 | })} 329 | A 330 |
331 |
332 | Imax 333 | ${(this.iMax).toLocaleString(undefined, { 334 | minimumFractionDigits: 3, 335 | minimumIntegerDigits: 2, 336 | useGrouping: false 337 | })} 338 | A 339 |
340 |
341 |
342 |
343 | Pout 344 | ${(this.info?.iOut * this.info?.vOut).toLocaleString(undefined, { 345 | minimumFractionDigits: 3, 346 | minimumIntegerDigits: 2, 347 | useGrouping: false 348 | })} 349 | W 350 |
351 |
352 | Pmax 353 | ${(this.pMax).toLocaleString(undefined, { 354 | minimumFractionDigits: 3, 355 | minimumIntegerDigits: 2, 356 | useGrouping: false 357 | })} 358 | W 359 |
360 |
361 | Eout 362 | ${(this.energy).toLocaleString(undefined, { 363 | minimumFractionDigits: 4, 364 | maximumFractionDigits: 4, 365 | minimumIntegerDigits: 1, 366 | useGrouping: false 367 | })} 368 | Wh 369 |
370 |
371 |
372 |
373 | ${this.renderMode()} 374 |
375 |
376 |
OPP
377 |
378 | OVP 379 | 382 | V 383 |
384 |
385 | OCP 386 | 389 | A 390 |
391 |
392 |
393 |
V
394 |
in 395 | ${this.info?.vIn.toLocaleString(undefined, { minimumFractionDigits: 3, minimumIntegerDigits: 2 })} V 396 |
397 |
maxOut 398 | ${this.info?.voMax.toLocaleString(undefined, { minimumFractionDigits: 3 })} V 399 |
400 |
401 |
402 |
T
403 |
404 | ${this.info?.temp1.toLocaleString(undefined, { minimumFractionDigits: 1 })} 405 | °C 406 |
407 |
408 | ${this.info?.temp2.toLocaleString(undefined, { minimumFractionDigits: 1 })} 409 | °C 410 |
411 |
412 |
413 | 414 |
415 |
416 | ` 417 | } 418 | 419 | renderMode () { 420 | if (!this.device) { 421 | return html` 422 | ` 423 | } 424 | if (!this.settings?.state) { 425 | return html` 426 | ` 427 | } 428 | switch (this.info?.outMode) { 429 | case 0: 430 | return html` 431 | ` 432 | case 1: 433 | return html` 434 | ` 435 | case 2: 436 | switch (this.info?.workSt) { 437 | case 1: 438 | return html` 439 | ` 440 | case 2: 441 | return html` 442 | ` 443 | } 444 | } 445 | } 446 | 447 | updated () { 448 | this.shadowRoot.querySelectorAll('input').forEach(input => { 449 | input.disabled = !this.device 450 | }) 451 | } 452 | 453 | togglePower () { 454 | this.setBasicOutput({ state: this.settings.state ? 0 : 1 }) 455 | } 456 | 457 | changeVoltage (event) { 458 | this.setBasicOutput({ vo_set: event.target.value }) 459 | } 460 | 461 | changeCurrent (event) { 462 | this.setBasicOutput({ io_set: event.target.value }) 463 | } 464 | 465 | changeOverVoltageProtection (event) { 466 | this.setBasicSettings({ ovp_set: event.target.value }) 467 | } 468 | 469 | changeOverCurrentProtection (event) { 470 | this.setBasicSettings({ ocp_set: event.target.value }) 471 | } 472 | 473 | reset () { 474 | this.energy = 0 475 | this.timer = Date.now() 476 | this.vMax = 0 477 | this.iMax = 0 478 | this.pMax = 0 479 | } 480 | 481 | firstUpdated () { 482 | const graphElement = this.shadowRoot.querySelector('#graph') 483 | this.graph = new uplot({ 484 | ...grapOptions, 485 | width: graphElement.offsetWidth, 486 | height: graphElement.offsetHeight - 48, 487 | }, [this.tHistory, this.vHistory, this.iHistory, this.pHistory], graphElement) 488 | } 489 | 490 | receiveBasicInfo ({ vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt }) { 491 | super.receiveBasicInfo({ vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt }) 492 | 493 | this.vMax = vOut > this.vMax ? vOut : this.vMax 494 | this.iMax = iOut > this.iMax ? iOut : this.iMax 495 | this.pMax = vOut * iOut > this.pMax ? vOut * iOut : this.pMax 496 | this.energy += vOut * iOut * (Date.now() - this.timer) / 1000 / 3600 497 | this.timer = Date.now() 498 | 499 | this.tHistory.push(Date.now() / 1000) // uplot uses seconds 500 | this.vHistory.push(vOut) 501 | this.iHistory.push(iOut) 502 | this.pHistory.push(vOut * iOut) 503 | if (this.vHistory.length > 30 * 1000 / this.refreshRate) { 504 | this.tHistory.shift() 505 | this.vHistory.shift() 506 | this.iHistory.shift() 507 | this.pHistory.shift() 508 | } 509 | this.graph.setData([this.tHistory, this.vHistory, this.iHistory, this.pHistory]) 510 | } 511 | } 512 | 513 | customElements.define('dp100-element', DP100Element) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DP100 WebApp 6 | 7 | 8 | 9 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DP100 Digital Power Supply", 3 | "description": "Web application for DP100 Digital Power Supply.", 4 | "short_name": "DP100", 5 | "theme_color": "#03a2e9", 6 | "background_color": "#03a2e9", 7 | "display": "fullscreen", 8 | "scope": "/DP100-WebApp/", 9 | "start_url": "/DP100-WebApp/", 10 | "icons": [ 11 | { 12 | "purpose": "maskable", 13 | "sizes": "512x512", 14 | "src": "assets/icons/icon512_maskable.png", 15 | "type": "image/png" 16 | }, 17 | { 18 | "purpose": "any", 19 | "sizes": "512x512", 20 | "src": "assets/icons/icon512_rounded.png", 21 | "type": "image/png" 22 | } 23 | ] 24 | } --------------------------------------------------------------------------------