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