├── Node ├── mwSerial.js └── package.json ├── Python ├── mw0582_algo.py └── mw0582_gui.py ├── README.md └── serial application └── sscom5.13.1.exe /Node/mwSerial.js: -------------------------------------------------------------------------------- 1 | const serialport = require('serialport'); 2 | const Readline = require('@serialport/parser-readline'); 3 | 4 | var portName = process.argv[2]; 5 | 6 | /* 7 | Port Connection 8 | */ 9 | 10 | var parser = new Readline({delimiter: "\n"}); 11 | 12 | function listPorts() { 13 | serialport.list().then( 14 | ports => { 15 | ports.forEach(port => { 16 | console.log(`${port.path}`); 17 | })}, 18 | err => { 19 | console.error('Error listing ports', err); 20 | } 21 | ) 22 | } 23 | 24 | var myPort = serialport(portName, { baudRate: 512000 }); 25 | 26 | myPort.pipe(parser); 27 | 28 | parser.on('data', function(line) { 29 | console.log(line) 30 | }) 31 | -------------------------------------------------------------------------------- /Node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mwNode", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "serialport": "latest" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Python/mw0582_algo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def movingAverage(data, exp, avg): 4 | new_avg = data * exp + (1 - exp) * avg 5 | return new_avg 6 | 7 | def impulseRemoval(arr, size): 8 | m = np.mean(arr) 9 | for x in arr: 10 | if x > mean: 11 | pos = pos + 1 12 | diff = diff + x - mean 13 | elif x < mean: 14 | neg = neg + 1 15 | correct = mean + (pos - neg) * diff / size**2 16 | return correct 17 | 18 | def dcRemoval(data, exp, avg): 19 | new_avg = data * exp + (1 - exp) * avg 20 | base = data - new_avg 21 | return base, new_avg 22 | 23 | def envelopExtract(data, w_dc, w_env, avg, env_avg): 24 | base, new_dc_avg = dcRemoval(data, w_dc, avg) 25 | new_env_avg = movingAverage(np.abs(base), w_env, env_avg) 26 | return new_dc_avg, new_env_avg 27 | 28 | -------------------------------------------------------------------------------- /Python/mw0582_gui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import matplotlib 3 | matplotlib.use("TkAgg") 4 | from matplotlib import pyplot as plt 5 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 6 | import matplotlib.animation as animation 7 | import serial 8 | from serial.tools.list_ports import comports 9 | import numpy as np 10 | import re 11 | from mw0582_algo import * 12 | 13 | #If we are using python 2.7 or under 14 | if sys.version_info[0] < 3: 15 | import Tkinter as tk 16 | import ttk 17 | #If we are using python 3.0 or above 18 | elif sys.version_info[0] >= 3: 19 | import tkinter as tk 20 | from tkinter import ttk 21 | 22 | 23 | class SensingGUI(tk.Frame): 24 | """docstring for SensingGUI""" 25 | 26 | def define_var(self): 27 | 28 | self.s = None 29 | 30 | self.triggerVar = tk.StringVar(self) 31 | self.triggerVar.set("Trigger") 32 | self.opBoxVar = tk.StringVar(self) 33 | self.opBoxVar.set("-") 34 | self.labelVar = tk.StringVar(self) 35 | self.labelVar.set("Not connected.") 36 | 37 | self.avgCheck = tk.IntVar(self) 38 | self.irCheck = tk.IntVar(self) 39 | self.dcrCheck = tk.IntVar(self) 40 | self.envCheck = tk.IntVar(self) 41 | 42 | self.checkButtons = [self.avgCheck,self.irCheck,self.dcrCheck,self.envCheck] 43 | 44 | def app_quit(self): 45 | self.master.destroy() 46 | self.master.quit() 47 | if self.s is not None: 48 | self.s.flush() 49 | self.s.close() 50 | 51 | def create_widgets(self, plotObject): 52 | self.menubar = tk.Menu(self.master) 53 | self.filemenu = tk.Menu(self.menubar, tearoff=0) 54 | self.filemenu.add_command(label="Exit", command=self.app_quit) 55 | 56 | # Left Frame 57 | self.leftFrame = tk.Frame(self.master, padx=10, pady=10) 58 | self.leftFrame.grid(row=0, column=0, sticky='N,W,S,E', rowspan=20) 59 | 60 | # Plot Frame 61 | # For data visualization 62 | 63 | self.plotFrame = tk.LabelFrame(self.leftFrame, padx=10, pady=10, borderwidth=2, text='Raw Plot', labelanchor='nw', relief=tk.RIDGE) 64 | self.plotFrame.grid(row=0, column=0) 65 | 66 | 67 | self.canvas = FigureCanvasTkAgg(plotObject.drawArea, master=self.plotFrame) 68 | self.canvas._tkcanvas.grid(row=0, column=0) 69 | self.canvas.draw() 70 | 71 | # Right Frame 72 | self.rightFrame = tk.Frame(self.master, padx=10, pady=10) 73 | self.rightFrame.grid(row=0, column=1, sticky='N,W,S,E', rowspan=20) 74 | 75 | self.operFrame = tk.Frame(self.rightFrame, padx=10, pady=10) 76 | self.operFrame.grid(row=0, column=0, columnspan=2, sticky='w,e') 77 | 78 | if sys.platform.startswith('darwin'): 79 | # portChoices = tuple([p[0].replace('/cu.', '/tty.') for p in comports() if p[0].find('SLAB') >= 0]) 80 | portChoices = tuple([p[0].replace('/cu.', '/tty.') for p in comports()]) 81 | else: 82 | portChoices = tuple([p[0].replace('/cu.', '/tty.') for p in comports()]) 83 | 84 | self.option = tk.OptionMenu(self.operFrame, self.opBoxVar, *portChoices) 85 | self.option.grid(row=0, column=0,sticky='w') 86 | 87 | def optionChanged(*args): 88 | print(self.opBoxVar.get()) 89 | self.s = serial.Serial(self.opBoxVar.get(), 512000) 90 | self.s.flush() 91 | if self.s is not None: 92 | self.labelVar.set("Device connected.") 93 | self.runButton.config(state=tk.NORMAL) 94 | self.rfLabel.config(state=tk.NORMAL) 95 | # self.rfScale.config(state=tk.NORMAL) 96 | self.powerLabel.config(state=tk.NORMAL) 97 | self.powerScale.config(state=tk.NORMAL) 98 | self.gainLabel.config(state=tk.NORMAL) 99 | self.gainScale.config(state=tk.NORMAL) 100 | self.delayLabel.config(state=tk.NORMAL) 101 | self.delayScale.config(state=tk.NORMAL) 102 | self.avgCheckbutton.config(state=tk.NORMAL) 103 | # self.avgTapEntry.config(state=tk.NORMAL) 104 | self.irCheckbutton.config(state=tk.NORMAL) 105 | self.irTapEntry.config(state=tk.NORMAL) 106 | self.dcrCheckbutton.config(state=tk.NORMAL) 107 | # self.dcrTapEntry.config(state=tk.NORMAL) 108 | self.envCheckbutton.config(state=tk.NORMAL) 109 | # self.envTapEntry.config(state=tk.NORMAL) 110 | 111 | self.opBoxVar.trace("w", optionChanged) 112 | 113 | # Connection status label 114 | self.connLabel = tk.Label(self.operFrame, textvariable=self.labelVar) 115 | self.connLabel.grid(row=0, column=1,sticky='w') 116 | 117 | # Button 118 | self.pause = True 119 | self.ani = None 120 | 121 | def trigger(): 122 | self.pause ^= True 123 | if self.pause: 124 | self.ani.event_source.stop() 125 | self.triggerVar.set("Trigger") 126 | else: 127 | self.ani = animation.FuncAnimation(plotObject.drawArea, plotObject.update, fargs=(self.s, self.checkButtons, plotObject.a0,), interval=1, blit=True) 128 | self.triggerVar.set("Pause") 129 | 130 | self.runButton = tk.Button(self.operFrame, textvariable=self.triggerVar, width=10, height=3, fg='red', command=trigger) 131 | # self.calibButton = tk.Button(self.operFrame, text='Re-Calibrate', width=10, height=3) 132 | self.runButton.grid(row=1, column=0,sticky='w') 133 | self.runButton.config(state=tk.DISABLED) 134 | # self.calibButton.grid(row=1, column=1,sticky='w') 135 | 136 | # Setting Frame 137 | self.settingFrame = tk.LabelFrame(self.rightFrame, padx=10, pady=10, borderwidth=2, text='Setting', labelanchor='nw', relief=tk.RIDGE) 138 | self.settingFrame.grid(row=1, column=0, sticky='w,e') 139 | 140 | # RF Range setting 141 | self.rfLabel = tk.Label(self.settingFrame, text='RF Range (in GHz)') 142 | self.rfLabel.grid(row=0, column=0, sticky='s,e') 143 | self.rfLabel.config(state=tk.DISABLED) 144 | self.rfScale = tk.Scale(self.settingFrame, 145 | from_ = 5.725, 146 | to = 5.875, 147 | orient = tk.HORIZONTAL, 148 | showvalue = 1, 149 | length = 200, 150 | resolution = 0.005, 151 | command = self.rf_scale_click 152 | ) 153 | self.rfScale.grid(row=0, column=1) 154 | self.rfScale.config(state=tk.DISABLED) 155 | # TX Power setting 156 | self.powerLabel = tk.Label(self.settingFrame, text='TX Power') 157 | self.powerLabel.grid(row=1, column=0, sticky='s,e') 158 | self.powerLabel.config(state=tk.DISABLED) 159 | self.powerScale = tk.Scale(self.settingFrame, 160 | from_ = 0, 161 | to = 7, 162 | orient = tk.HORIZONTAL, 163 | showvalue = 1, 164 | length = 200, 165 | resolution = 1, 166 | command = self.power_scale_click 167 | ) 168 | self.powerScale.grid(row=1, column=1) 169 | self.powerScale.config(state=tk.DISABLED) 170 | 171 | # RX Gain setting 172 | self.gainLabel = tk.Label(self.settingFrame, text='RX Gain') 173 | self.gainLabel.grid(row=2, column=0,sticky='s,e') 174 | self.gainLabel.config(state=tk.DISABLED) 175 | 176 | self.gainScale = tk.Scale(self.settingFrame, 177 | from_ = 0, 178 | to = 7, 179 | orient = tk.HORIZONTAL, 180 | showvalue = 1, 181 | length = 200, 182 | resolution = 1, 183 | command = self.gain_scale_click 184 | ) 185 | self.gainScale.grid(row=2, column=1) 186 | self.gainScale.config(state=tk.DISABLED) 187 | 188 | # Delay setting 189 | self.delayLabel = tk.Label(self.settingFrame, text='Delay Time (in s)') 190 | self.delayLabel.grid(row=3, column=0,sticky='s,e') 191 | self.delayLabel.config(state=tk.DISABLED) 192 | self.delayScale = tk.Scale(self.settingFrame, 193 | from_ = 0, 194 | to = 3599, 195 | orient = tk.HORIZONTAL, 196 | showvalue = 1, 197 | length = 200, 198 | resolution = 100, 199 | command = self.delay_scale_click 200 | ) 201 | self.delayScale.grid(row=3, column=1) 202 | self.delayScale.config(state=tk.DISABLED) 203 | 204 | # Processing Frame 205 | self.processFrame = tk.LabelFrame(self.rightFrame, padx=10, pady=10, borderwidth=2, text='Process', labelanchor='nw', relief=tk.RIDGE) 206 | self.processFrame.grid(row=2, column=0, columnspan=3, sticky='w,e') 207 | 208 | # Process Selection 209 | 210 | self.avgCheckbutton = tk.Checkbutton(self.processFrame, 211 | text = 'Moving Average', 212 | variable = self.avgCheck, 213 | onvalue = 1, 214 | offvalue = 0, 215 | command = self.avgcheckbutton_click 216 | ) 217 | self.avgCheckbutton.grid(row=0, column=0, sticky='w') 218 | 219 | self.irCheckbutton = tk.Checkbutton(self.processFrame, 220 | text = 'Impulse Removal', 221 | variable = self.irCheck, 222 | onvalue = 1, 223 | offvalue = 0, 224 | command = self.ircheckbutton_click 225 | ) 226 | self.irCheckbutton.grid(row=1, column=0, sticky='w') 227 | # self.irCheckbutton.config(state=tk.DISABLED) 228 | self.irLabel = tk.Label(self.processFrame, text='Taps:') 229 | self.irLabel.grid(row=1, column=1) 230 | self.irTapEntry = tk.Entry(self.processFrame, relief=tk.RIDGE, width=10) 231 | self.irTapEntry.insert(0, '5') 232 | self.irTapEntry.grid(row=1, column=2, sticky='e') 233 | self.irTapEntry.config(state=tk.DISABLED) 234 | 235 | self.dcrCheckbutton = tk.Checkbutton(self.processFrame, 236 | text = 'DC Removal', 237 | variable = self.dcrCheck, 238 | onvalue = 1, 239 | offvalue = 0, 240 | command = self.dcrcheckbutton_click 241 | ) 242 | self.dcrCheckbutton.grid(row=2, column=0, sticky='w') 243 | # self.dcrCheckbutton.config(state=tk.DISABLED) 244 | # self.dcrLabel = tk.Label(self.processFrame, text='Taps:') 245 | # self.dcrLabel.grid(row=2, column=1) 246 | # self.dcrTapEntry = tk.Entry(self.processFrame, relief=tk.RIDGE, width=10) 247 | # self.dcrTapEntry.insert(0, '5') 248 | # self.dcrTapEntry.grid(row=2, column=2, sticky='e') 249 | # self.dcrTapEntry.config(state=tk.DISABLED) 250 | 251 | self.envCheckbutton = tk.Checkbutton(self.processFrame, 252 | text = 'Envelop Extraction', 253 | variable = self.envCheck, 254 | onvalue = 1, 255 | offvalue = 0, 256 | command = self.envcheckbutton_click 257 | ) 258 | self.envCheckbutton.grid(row=3, column=0, sticky='w') 259 | # self.envCheckbutton.config(state=tk.DISABLED) 260 | # self.envLabel = tk.Label(self.processFrame, text='Taps:') 261 | # self.envLabel.grid(row=3, column=1) 262 | # self.envTapEntry = tk.Entry(self.processFrame, relief=tk.RIDGE, width=10) 263 | # self.envTapEntry.insert(0, '5') 264 | # self.envTapEntry.grid(row=3, column=2, sticky='e') 265 | # self.envTapEntry.config(state=tk.DISABLED) 266 | 267 | def rf_scale_click(self, v): 268 | if self.s is not None: 269 | command = "AT+FREQ={}".format(v) 270 | self.s.write(command.encode('utf-8')) 271 | line = self.s.read(2) 272 | if line is 'OK': 273 | tk.messagebox.showerror("Error", "Parameter set failed.") 274 | else: 275 | tk.messagebox.showinfo("Success", "Parameter set successful!") 276 | def power_scale_click(self, v): 277 | if self.s is not None: 278 | command = "AT+PA={}".format(v) 279 | self.s.write(command.encode('utf-8')) 280 | def gain_scale_click(self,v): 281 | if self.s is not None: 282 | command = "AT+REVGAIN={}".format(v) 283 | self.s.write(command.encode('utf-8')) 284 | def delay_scale_click(self, v): 285 | if self.s is not None: 286 | command = "AT+DELAY={}".format(v) 287 | self.s.write(command.encode('utf-8')) 288 | def avgcheckbutton_click(self): 289 | self.irCheckbutton.deselect() 290 | self.dcrCheckbutton.deselect() 291 | self.envCheckbutton.deselect() 292 | if(self.avgCheck.get()): 293 | self.avgCheckbutton.select() 294 | else: 295 | self.avgCheckbutton.deselect() 296 | 297 | def ircheckbutton_click(self, plotObject): 298 | self.dcrCheckbutton.deselect() 299 | self.envCheckbutton.deselect() 300 | self.avgCheckbutton.deselect() 301 | self.irCheckbutton.select() 302 | if(self.irCheck.get()): 303 | self.irCheckbutton.select() 304 | else: 305 | self.irCheckbutton.deselect() 306 | 307 | def dcrcheckbutton_click(self): 308 | self.irCheckbutton.deselect() 309 | self.envCheckbutton.deselect() 310 | self.avgCheckbutton.deselect() 311 | if(self.dcrCheck.get()): 312 | self.dcrCheckbutton.select() 313 | else: 314 | self.dcrCheckbutton.deselect() 315 | 316 | def envcheckbutton_click(self): 317 | self.irCheckbutton.deselect() 318 | self.dcrCheckbutton.deselect() 319 | self.avgCheckbutton.deselect() 320 | if(self.envCheck.get()): 321 | self.envCheckbutton.select() 322 | else: 323 | self.envCheckbutton.deselect() 324 | 325 | def __init__(self, master=None): 326 | tk.Frame.__init__(self,master) 327 | self.appPlot = GUIPlot(size=500) 328 | self.master = master 329 | self.define_var() 330 | self.create_widgets(self.appPlot) 331 | 332 | 333 | class GUIPlot: 334 | # constructor 335 | def __init__(self, size): 336 | self.drawArea = plt.figure() 337 | self.ax = plt.axes(ylim=(0, 2000)) 338 | self.ax.axvline(linewidth=4, color='r', x=250) 339 | self.arr = np.zeros(size) 340 | self.a0, = self.ax.plot(range(500), self.arr) 341 | self.avg = 0; 342 | self.dcavg = 0; 343 | self.env_avg = 0; 344 | self.base = 0; 345 | 346 | # update plot 347 | def update(self, frameNum, ser, checkButtons, a0): 348 | try: 349 | line = ser.read(300) 350 | try: 351 | data = [int(x, 16) for x in re.findall(' (?!f)\w{4}', line.decode('utf-8'))] 352 | # print(data) 353 | # self.arr = np.append(self.arr[len(data):], data) 354 | new_data = data[-1] 355 | 356 | if(checkButtons[0].get()): 357 | self.avg = movingAverage(self.arr[250], 0.2, self.avg) 358 | self.arr = np.concatenate((self.arr[1:250], self.avg, self.arr[251:], new_data), axis=None) 359 | elif(checkButtons[1].get()): 360 | pass 361 | elif(checkButtons[2].get()): 362 | self.base, self.dcavg = dcRemoval(self.arr[250], 0.7, self.dcavg) 363 | self.arr = np.concatenate((self.arr[1:250], self.base, self.arr[251:], new_data), axis=None) 364 | elif(checkButtons[3].get()): 365 | self.dcavg, self.env_avg = envelopExtract(self.arr[250], 0.2, 0.7, self.dcavg, self.env_avg) 366 | self.arr = np.concatenate((self.arr[1:250], self.env_avg, self.arr[251:], new_data), axis=None) 367 | else: 368 | self.arr = np.concatenate((self.arr[1:], new_data), axis=None) 369 | 370 | self.a0.set_ydata(self.arr) 371 | 372 | except Exception: 373 | pass 374 | except KeyboardInterrupt: 375 | print('exiting..') 376 | return self.a0, 377 | 378 | if __name__ == '__main__': 379 | root = tk.Tk() 380 | root.title('Maxustech Sensing GUI') 381 | app = SensingGUI(master=root) 382 | 383 | def on_close(): 384 | try: 385 | app.app_quit() 386 | except Exception: 387 | print("Exiting...") 388 | app.app_quit() 389 | 390 | root.protocol("WM_DELETE_WINDOW", on_close) 391 | app.mainloop() 392 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MWTool for mw series radar 2 | 3 | 4 | 5 |

6 | 7 |

8 | 9 | This repo is a debug tool for developers to visualize the raw data of Maxustech mw series radar. MW series radar is a highly integrated signle chip motion sensor. 10 | 11 | Interested users are welcome to join [this](https://join.slack.com/t/maxusdev/shared_invite/zt-mk99v1px-gywUzv6yQlYTUQdme29XsA) slack group for further support. 12 | 13 | # Introduction 14 | 15 | The MW0582TR11 is a highly integrated single chip 5.8GHz microwave motion sensor developed by Maxustech. The MW0582TR11 simplifies the implementation of non-contact detection applications and is an ideal solution for smart lighting, surveillance, automation and any self-monitored radar system. 16 | 17 | The module achieves broad sensing coverage given the well-designed 1Tx, 1Rx transceiver antenna and robust performance in anti-interference with built-in PLL. Simple programming model changes can enable a wide variety of sensor implementation (Short, Mid, Long) with the possibility of dynamic reconfiguration for implementing a multimode sensor. 18 | 19 | ## Working Principle 20 | 21 |

22 | 23 |

24 | 25 | MW0582TR11 is based on doppler principle. It emits 5.8GHz radio signal and collects the echo. Mixing is already done by the device and the raw data is sampled at the rate of 625Hz. If there's no motion, the raw data will stay around a constant number. However, if there's motion detected, the raw data will start to fluctuate. Users can set a threshold at the raw data for simple detection application. 26 | 27 | 28 | ## Usage 29 | 30 | ### Python (Only tested on v3.7.4) 31 | 32 | > Dependencies: 33 | > 34 | > - Matplotlib 35 | > - Numpy 36 | > - Tkinter 37 | 38 | ``` python 39 | python mw0582_gui.py 40 | ``` 41 | 42 | ![Xnip2019-12-05_19-17-28.png](https://i.loli.net/2019/12/05/NFYnSeI4XfypODc.png) 43 | 44 | #### Note: 45 | 46 | - Impulse removal algorithm is yet to finish 47 | - RF setting is not available at in this version 48 | 49 | ### Node 50 | 51 | > Dependencies: 52 | > 53 | > - serialport 54 | 55 | - Install Node v8.9.1 and above 56 | - Run `npm install` to install the project dependencies specified in `package.json` 57 | - Run `node mwSerial.js ` to capture the raw data output 58 | 59 | ### Other serial tools 60 | 61 | Recommended serial tools include: 62 | 63 | - **Baudrate at 512000** 64 | 65 | - **CH340/CP2102 USB-TTL** adapter are recommended 66 | 67 | - For mac, `coolterm`is recommended. To change the baudrate simply goto **"Option-Serial Port-Baudrate(Custom)"** 68 | ![Change the baudrate](https://i.loli.net/2020/01/06/tOy9frKPDdCE4Al.png) 69 | 70 | While using **Coolterm**, follow the instruction to set the terminal to *Line mode*. Line mode doesn't send data until enter has been pressed. Raw mode sends characters directly to the screen. 71 | ![](https://i.loli.net/2020/02/08/FfkESQmvrRGhLCW.png) 72 | 73 | - For windows, `sscom32` will be a nice choice. The language is set to Chinese by default. Check the checkbox to change to english and set baudrate to 512000. 74 | 75 | ![sscom32](https://i.loli.net/2020/02/08/8EzyiBthT5UQDa9.png) 76 | 77 | #### Debug commands 78 | 79 | - Radiated power `AT+PA=x` 80 | The radiated power has 7 levels (0000~0007) for developer to choose. With the bigger number the device radiated more power. 81 | - Receive gain `AT+REVGAIN=x` 82 | The receive gain has 7 levels (0000~0007) for developer to choose. With the bigger number the device becomes more sensitive. 83 | - Time delay `AT+DELAY=x` 84 | The time delay represent how long VOUT stays high after the device detects object. Developers can choose number from 0001~3599 (unit in second). 85 | - Working mode `AT+DEBUG=x` 86 | By default the device works at normal mode, x = 0000. Developer can use x = 0002 to fetch raw radar data. 87 | - Detect threshold `AT+THRES=x` 88 | The threshold (0001~0099) represents how easy the device to trigger detection. With lower number the device is more inclined to trigger detection which could be a false alarm. 89 | 90 | Radiated power, receive gain and detection threshold have a complicated relationship and should be set probably according to your situation to ensure proper functioning of the device. 91 | -------------------------------------------------------------------------------- /serial application/sscom5.13.1.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxusDev/MWTool/4e012b88c4620bf47ae316fd08939a87541d48c8/serial application/sscom5.13.1.exe --------------------------------------------------------------------------------