Skip to content

Commit ced9c7c

Browse files
committed
Push initial weekend code
1 parent 4e34e0a commit ced9c7c

5 files changed

Lines changed: 323 additions & 1 deletion

File tree

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[*]
2+
max_line_length = 120
3+
indent_style = space
4+
indent_size = 2
5+
end_of_line = lf

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: codingjoe

.gitignore

Whitespace-only changes.

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
1-
# DP100-WebApp
1+
# DP100 WebApp
2+
23
A browser interface for the DP100 lab power supply.
4+
5+
## Usage
6+
7+
You don't need to install anything to use this webapp.
8+
Just visit [this link](https://johannes.maron.family/DP100-WebApp/) and you're good to go.
9+
10+
## Development
11+
12+
If you want to contribute to this project, you can clone this repository and open the `index.html` file in your browser.
13+
14+
## Credits
15+
16+
Special thanks for @scottbez1 for inspiring this project and being an overall camp.

index.html

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charSet="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>DP100 WebApp</title>
7+
<link href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" rel="stylesheet">
8+
<style>
9+
.u-value {
10+
font-family: monospace;
11+
}
12+
</style>
13+
</head>
14+
<body>
15+
<button>connect</button>
16+
<script type="module">
17+
import uplot from 'https://cdn.jsdelivr.net/npm/uplot@1.6.31/+esm'
18+
19+
const vendorId = 11836, productId = 44801 // DP100's HID IDs
20+
const deviceAddr = 251 // DP100's device address
21+
22+
/**
23+
* Calculate the buffers CRC-16/MODBUS checksum.
24+
*
25+
* @param {ArrayBuffer} buffer - The buffer to calculate the CRC16 for.
26+
* @return {number} - The CRC16 checksum.
27+
*/
28+
function crc16 (buffer) {
29+
let crc = 0xFFFF
30+
31+
for (const byte of new Uint8Array(buffer)) {
32+
crc = crc ^ byte
33+
34+
for (let j = 0; j < 8; j++) {
35+
const odd = crc & 0x0001
36+
crc = crc >> 1
37+
if (odd) {
38+
crc = crc ^ 0xA001
39+
}
40+
}
41+
}
42+
43+
return crc
44+
}
45+
46+
/** DP100 Modbus Function IDs */
47+
const FUNCTIONS = Object.freeze({
48+
DEVICE_INFO: 0x10,
49+
FIRM_INFO: 17,
50+
START_TRANS: 18,
51+
DATA_TRANS: 19,
52+
END_TRANS: 20,
53+
DEV_UPGRADE: 21,
54+
BASIC_INFO: 48,
55+
BASIC_SET: 53,
56+
SYSTEM_INFO: 0x40,
57+
SYSTEM_SET: 69,
58+
SCAN_OUT: 80,
59+
SERIAL_OUT: 85,
60+
DISCONNECT: 0x80,
61+
NONE: 0xFF,
62+
})
63+
64+
const WORK_MODES = Object.freeze({
65+
CC: 0,
66+
CV: 1,
67+
OFF: 2,
68+
})
69+
70+
const WORK_MODE_MAP = {
71+
[WORK_MODES.CV]: 'Constant Voltage',
72+
[WORK_MODES.CC]: 'Constant Current',
73+
[WORK_MODES.OFF]: 'Off',
74+
}
75+
76+
/** DP100 device class.
77+
*
78+
* This class is used to interact with the DP100 power supply.
79+
* @example
80+
*
81+
* class MyPSU extends DP100 {
82+
* receiveBasicInfo ({vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt}) {
83+
* console.info('Input Voltage:', vIn, 'V')
84+
* console.info('Output Voltage:', vOut, 'V')
85+
* console.info('Output Current:', iOut, 'A')
86+
* console.info('Max Output Voltage:', voMax, 'V')
87+
* console.info('Temperature 1:', temp1, '°C')
88+
* console.info('Temperature 2:', temp2, '°C')
89+
* console.info('DC 5V:', dc5V, 'V')
90+
* console.info('Output Mode:', WORK_MODE_MAP[outMode])
91+
* console.info('Work State:', workSt)
92+
* }
93+
* }
94+
*
95+
* const psu = new MyPSU()
96+
* await psu.connect()
97+
*/
98+
class DP100 {
99+
100+
device = null
101+
102+
/** Connect to the DP100 device. */
103+
async connect () {
104+
[this.device] = await navigator.hid.requestDevice({
105+
filters: [{ vendorId, productId }]
106+
})
107+
await this.device.open()
108+
this.device.addEventListener('inputreport', this.inputReportHandler.bind(this))
109+
setInterval(
110+
() => this.sendReport(FUNCTIONS.BASIC_INFO), 10
111+
)
112+
}
113+
114+
/** Send a report to the DP100
115+
* @param {Number} functionId -- The function to call on the DP100.
116+
* @param {Uint8Array} content -- The data to send to the DP100.
117+
*/
118+
async sendReport (functionId, content = null) {
119+
content = content || new Uint8Array(0)
120+
const report = new Uint8Array([
121+
deviceAddr,
122+
functionId,
123+
content.length,
124+
content,
125+
0, // checksum
126+
0 // checksum
127+
])
128+
const reportView = new DataView(report.buffer, report.byteOffset, report.byteLength)
129+
const checksum = crc16(report.buffer.slice(0, report.length - 2))
130+
reportView.setUint16(report.length - 2, checksum, true)
131+
console.debug('device.sendReport', reportView)
132+
return this.device.sendReport(0, report)
133+
}
134+
135+
/** Handle input reports from the DP100
136+
* @param {HIDInputReportEvent} event
137+
*/
138+
inputReportHandler (event) {
139+
console.debug('device.inputreport', event)
140+
const data = event.data
141+
const headerLength = 4
142+
const header = {
143+
deviceAddr: data.getUint8(0),
144+
functionType: event.data.getUint8(1),
145+
sequence: event.data.getUint8(2),
146+
contentLength: event.data.getUint8(3),
147+
}
148+
const contentView = new DataView(data.buffer.slice(headerLength, headerLength + header.contentLength))
149+
const checksum = data.getUint16(headerLength + header.contentLength, true)
150+
const computedChecksum = crc16(data.buffer.slice(0, headerLength + header.contentLength))
151+
if (computedChecksum !== checksum) {
152+
console.error('Checksum Failed', {
153+
expected: computedChecksum.toString(16),
154+
received: checksum.toString(16)
155+
})
156+
return
157+
}
158+
console.debug('content', contentView)
159+
160+
switch (header.functionType) {
161+
case FUNCTIONS.BASIC_INFO:
162+
const basicInfo = {
163+
vIn: contentView.getUint16(0, true) / 1000,
164+
vOut: contentView.getUint16(2, true) / 1000,
165+
iOut: contentView.getUint16(4, true) / 1000,
166+
voMax: contentView.getUint16(6, true) / 1000,
167+
temp1: contentView.getUint16(8, true) / 10,
168+
temp2: contentView.getUint16(10, true) / 10,
169+
dc5V: contentView.getUint16(12, true) / 1000,
170+
outMode: contentView.getUint8(14),
171+
workSt: contentView.getUint8(15)
172+
}
173+
this.receiveBasicInfo(basicInfo)
174+
break
175+
default:
176+
console.warn('Unhandled function', header.functionType)
177+
}
178+
}
179+
180+
/** Handle basic info from the DP100
181+
* @param {Object} basicInfo
182+
* @param {Number} basicInfo.vIn - Input voltage in mV.
183+
* @param {Number} basicInfo.vOut - Output voltage in mV.
184+
* @param {Number} basicInfo.iOut - Output current in mA.
185+
* @param {Number} basicInfo.voMax - Max output voltage in mV.
186+
* @param {Number} basicInfo.temp1 - Temperature 1 in 0.1°C.
187+
* @param {Number} basicInfo.temp2 - Temperature 2 in 0.1°C.
188+
* @param {Number} basicInfo.dc5V - 5V rail in mV.
189+
* @param {Number} basicInfo.outMode - Output mode.
190+
* @param {Number} basicInfo.workSt - Work state.
191+
*/
192+
receiveBasicInfo ({ vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt }) {
193+
const BasicInfoEvent = new CustomEvent('basicInfo', {
194+
detail: { vIn, vOut, iOut, voMax, temp1, temp2, dc5V, outMode, workSt }
195+
})
196+
document.dispatchEvent(BasicInfoEvent)
197+
}
198+
}
199+
200+
let opts = {
201+
title: 'UV Graph',
202+
id: 'uv-graph',
203+
width: 800,
204+
height: 600,
205+
series: [
206+
{
207+
label: 'Time',
208+
value: (self, rawValue) => rawValue === null ? ' N/A' : new Date(rawValue * 1000).toLocaleTimeString(),
209+
},
210+
{
211+
show: true,
212+
spanGaps: true,
213+
label: 'Voltage',
214+
value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'V',
215+
scale: 'V',
216+
stroke: 'rgb(250, 200, 0)',
217+
width: 2,
218+
}, {
219+
show: true,
220+
spanGaps: true,
221+
label: 'Current',
222+
value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'A',
223+
scale: 'A',
224+
stroke: 'green',
225+
width: 2,
226+
}, {
227+
show: true,
228+
spanGaps: true,
229+
label: 'Power',
230+
value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'W',
231+
scale: 'W',
232+
fill: 'rgba(200, 0, 200, 0.3)',
233+
width: 0,
234+
}
235+
],
236+
axes: [
237+
{
238+
show: false
239+
},
240+
{
241+
scale: 'V',
242+
label: 'Voltage (V)',
243+
value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'V',
244+
grid: { show: false },
245+
},
246+
{
247+
scale: 'A',
248+
label: 'Current (A)',
249+
value: (self, rawValue) => rawValue === null ? 'N/A' : rawValue.toLocaleString(undefined, { minimumFractionDigits: 3 }) + 'A',
250+
side: 1,
251+
grid: { show: false },
252+
},
253+
{},
254+
],
255+
scales: {
256+
'x': {},
257+
'V': {
258+
auto: false,
259+
range: [0, 30],
260+
},
261+
'A': {
262+
auto: false,
263+
range: [0, 5],
264+
},
265+
'W': {
266+
auto: false,
267+
range: [0, 100],
268+
}
269+
},
270+
}
271+
272+
const tHistory = []
273+
const vHistory = []
274+
const iHistory = []
275+
const wHistory = []
276+
const data = [tHistory, vHistory, iHistory, wHistory]
277+
278+
const graph = new uplot(opts, data, document.body)
279+
280+
document.querySelector('button').addEventListener('click', async () => {
281+
const dp100 = new DP100()
282+
await dp100.connect()
283+
284+
document.addEventListener('basicInfo', (event) => {
285+
const { vOut, iOut } = event.detail
286+
tHistory.push(Date.now() / 1000)
287+
vHistory.push(vOut)
288+
iHistory.push(iOut)
289+
wHistory.push(vOut * iOut)
290+
if (vHistory.length > 1200) {
291+
tHistory.shift()
292+
vHistory.shift()
293+
iHistory.shift()
294+
wHistory.shift()
295+
}
296+
graph.setData(data)
297+
})
298+
299+
})
300+
</script>
301+
</body>
302+
</html>

0 commit comments

Comments
 (0)