1+ # pip install matplotlib numpy
2+ import socket
3+ import time
4+ import traceback
5+ import sys
6+ import os
7+ from itertools import cycle
8+ import binascii
9+ import _thread as thread
10+ import numpy as np
11+ import matplotlib .pyplot as plt
12+
13+
14+ IP = '192.168.0.10'
15+ PORT = 10940
16+ AUTORANGE = False
17+
18+ def partition (n : int , lst ):
19+ for i in range (0 , len (lst ), n ):
20+ yield lst [i :i + n ]
21+
22+
23+ def deg2theta (deg ):
24+ return deg / 360 * 2 * np .pi
25+
26+ def toCartesian (xTheta , xR ):
27+ X = np .cos (xTheta ) * xR
28+ Y = np .sin (xTheta ) * xR
29+ return X ,Y
30+
31+ class HokuyoReader ():
32+ measureMsgHeads = {'ME' , 'GE' , 'MD' , 'GD' }
33+
34+ def makeSocket (self , ip , port ):
35+ sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
36+ sock .connect ((ip , port ))
37+ sock .settimeout (5 )
38+ return sock
39+
40+ # decode 3 byte integer data line
41+ def decodeDistance (self , data ):
42+ # remove checksum bytes for every 65 bytes of data
43+ parts = ['' .join (part [:- 1 ]) for part in list (partition (65 , data ))]
44+ # repack data
45+ data = '' .join (parts )
46+ # transform 3-byte int
47+ ns = [ord (d ) - 0x30 for d in data ]
48+ ns = [f'{ d :06b} ' for d in ns ]
49+ ns = list (partition (3 , ns ))
50+ ns = [int ('' .join (c3 ), 2 ) for c3 in ns ]
51+ # set out-of-range value to zero
52+ ns = [0 if n == 65533 else n for n in ns ]
53+
54+ self .rDistance = np .array (ns )
55+ self .mStep = self .startStep
56+ self .measuring = False
57+ return ns
58+
59+ def __init__ (self , ip , port , startStep = 0 ):
60+ self .ip = ip
61+ self .port = port
62+ # For decoding measuring data
63+ self .measuring = False
64+ self .skip = 0
65+ self .head = None
66+ # Starting step id
67+ self .startStep = startStep
68+ # Current step id, for decoding polar distance data
69+ self .mStep = startStep
70+ # Array of distance
71+ self .rDistance = np .zeros (1081 - startStep , dtype = int )
72+ # Buffer to receive packets
73+ self .buf = ""
74+ self .expectedPacketSize = 65 * 50 + 44 # TODO hardcoded for full range measurement
75+
76+ ids = np .arange (1081 - startStep )
77+ self .xTheta = deg2theta ((ids + startStep ) * 270.0 / 1080 + 45 - 90 )
78+
79+ self .sock = self .makeSocket (ip , port )
80+ self .__startReader__ ()
81+
82+
83+ def send (self , cmd : str ):
84+ self .sock .sendall (cmd .encode ())
85+
86+
87+ def startPlotter (self ):
88+ plt .show ()
89+ fig = plt .figure ()
90+ axc = plt .subplot (121 )
91+ axp = plt .subplot (122 , projection = 'polar' )
92+ # axp.set_thetamax(deg2theta(45))
93+ # axp.set_thetamax(deg2theta(270 + 45))
94+ axp .grid (True )
95+ print ('Plotter started, press any key to exit' )
96+
97+ print (f'{ self .xTheta } , { self .rDistance } ' )
98+ while True :
99+ X , Y = toCartesian (self .xTheta , self .rDistance )
100+
101+ axp .clear ()
102+ axc .clear ()
103+
104+ axp .plot (self .xTheta , self .rDistance )
105+
106+ axc .plot (X , Y )
107+
108+ if not AUTORANGE :
109+ axp .set_rmax (8000 )
110+ axc .set_xlim (- 5000 , 5000 )
111+ axc .set_ylim (- 5000 , 5000 )
112+
113+ plt .pause (1e-17 )
114+
115+ if plt .waitforbuttonpress (timeout = 0.02 ):
116+ os ._exit (0 )
117+
118+
119+
120+ # Change hokuyo IP address, requires reboot
121+ def changeIP (self , ip : str , gateway : str , netmask = '255.255.255.0' ):
122+ def formatZeros (addr ):
123+ return '' .join ([n .rjust (3 , '0' ) for n in addr .split ('.' )])
124+
125+ ip = formatZeros (ip )
126+ gateway = formatZeros (gateway )
127+ netmask = formatZeros (netmask )
128+ cmd = f'$IP{ ip } { netmask } { gateway } \r \n '
129+ print (f'ChangeIP cmd: { cmd } ' )
130+ self .send (cmd )
131+
132+ # Start continous read mode
133+ def startContinuous (self , start : int , end : int , withIntensity = False ):
134+ head = 'ME' if withIntensity else 'MD'
135+ cmd = f'{ head } { start :04d} { end :04d} 00000\r \n '
136+ print (cmd )
137+ self .head = cmd .strip ()
138+ self .send (cmd )
139+
140+
141+ # Start single read
142+ def singleRead (self , start : int , end : int , withIntensity = False ):
143+ head = 'GE' if withIntensity else 'GD'
144+ cmd = f'{ head } { start :04d} { end :04d} 01000\r \n '
145+ self .send ( cmd )
146+
147+ def stop (self ):
148+ cmd = 'QT\r \n '
149+ self .send ( cmd )
150+
151+ def reboot (self ):
152+ cmd = 'RB\r \n '
153+ self .send (cmd )
154+ self .send (cmd )
155+
156+
157+ def handleMsgLine (self , line ):
158+ if line == self .head :
159+ self .measuring = True
160+ self .skip = 0
161+ self .mStep = self .startStep
162+ return True
163+
164+ if self .measuring :
165+ if self .skip < 2 :
166+ self .skip += 1
167+ return True
168+ else :
169+ self .buf += line .strip ()
170+ # print(f'buf size {len(self.buf)}')
171+ if len (self .buf ) >= self .expectedPacketSize :
172+ self .decodeDistance (self .buf )
173+ self .buf = ''
174+ return True
175+
176+ return False
177+
178+ def __startReader__ (self ):
179+ def handleMeasuring (msg ):
180+ lines = msg .split ()
181+ for line in lines :
182+ if not self .handleMsgLine (line ):
183+ print (f'ignore { line } ' )
184+
185+ def loop ():
186+ try :
187+ while True :
188+ try :
189+ m , _ = self .sock .recvfrom (1024 )
190+ msg = m .decode ()
191+ handleMeasuring (msg )
192+ except socket .timeout as e :
193+ print ('Read timeout, sensor disconnected?' )
194+ os ._exit (1 )
195+ finally :
196+ self .sock .close ()
197+
198+ thread .start_new_thread (loop , ())
199+
200+ if __name__ == '__main__' :
201+ sensor = HokuyoReader (IP , PORT )
202+ sensor .stop ()
203+ # sensor.singleRead(0, 1080)
204+ time .sleep (2 )
205+
206+ sensor .startContinuous (0 , 1080 )
207+ sensor .startPlotter ()
0 commit comments