/
blutil.py
executable file
·225 lines (196 loc) · 8.96 KB
/
blutil.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env python3
"""
blutil.py is a command line tool for programming BL-600SA "SmartBASIC" devices.
See http://projectgus.com/2014/03/laird-bl600-modules for more details
Copyright (C)2014 Angus Gratton, released under BSD license as per the LICENSE file.
"""
import argparse, serial, time, subprocess, sys, os, re, tempfile
parser = argparse.ArgumentParser(description='Perform various operations with a BL600 module. The -m option can only be used instead of -p when compiling using the -c flag. -p on the other hand is compatible with all other argument choices.')
device_arg = parser.add_mutually_exclusive_group(required=True)
device_arg.add_argument('-p', '--port', help="Serial port to connect to")
device_arg.add_argument('-m', '--model', help="Specify (instead of detecting) the model number, see command output for example model string")
parser.add_argument('-b', '--baud', type=int, default=9600, help="Baud rate for connection")
parser.add_argument('--no-dtr', action="store_true", help="Don't toggle the DTR line as a reset")
cmd_arg = parser.add_mutually_exclusive_group(required=True)
cmd_arg.add_argument('-c', '--compile', help="Compile specified smartBasic file to a .uwc file.", metavar="BASICFILE")
cmd_arg.add_argument('-l', '--load', help="Upload specified smartBasic file to BL600 (if argument is a .sb file it will be compiled first.)", metavar="FILE")
cmd_arg.add_argument('-r', '--run', help="Execute specified smartBasic file on BL600 (if argument is a .sb file it will be compiled and uploaded first, if argument is a .uwc file it will be uploaded first.)", metavar="FILE")
cmd_arg.add_argument('--ls', action="store_true", help="List all files uploaded to the BL600")
cmd_arg.add_argument('--rm', metavar="FILE", help="Remove specified file from the BL600")
cmd_arg.add_argument('--format', action="store_true", help="Erase all stored files from the BL600")
class RuntimeError(Exception):
pass
class BLDevice(object):
def __init__(self, args):
if args.model is None:
self.port = serial.Serial(args.port, args.baud, timeout=0.8)
def writecmd(self, args, expect_response=True, timeout=0.5):
self.port.write(bytearray("AT%s%s\r"%("" if args.startswith("+") else " ", args), "ascii"))
if not expect_response:
return
response = b''
start = time.time()
while not response.endswith(b"00\r") and time.time() < start + timeout:
response += self.port.read(1)
if response.endswith(b"00\r"):
return str(response, "ascii")[:-3].strip()
else:
if len(response) == 0:
raise RuntimeError("Got no response to command 'AT%s'. Not connected or not in interactive mode?" % args)
elif len(response) > 4 and response[0:4] == b'\n01\t':
errorcode = str(response[4:].decode())[:-1]
raise RuntimeError("BL600 returned error %s: %s" % (errorcode,get_errordesc(errorcode)))
else:
raise RuntimeError("Got unexpected/error response to command 'AT%s': %s" % (args,response))
def read_param(self, param):
return self.writecmd("I %d"%param).split("\t")[-1]
def detect_model(self):
model = self.read_param(0)
revision = self.read_param(13)
print("Detected model %s %s" % (model, revision))
self.model = "%s_%s" % (model, revision.replace(" ","_"))
def compile(self, filepath):
blutil_dir = os.path.dirname(sys.argv[0])
compiler = os.path.join(blutil_dir, "XComp_%s.exe" % (self.model,))
if not os.path.exists(compiler):
raise RuntimeError("Compiler not found at %s. Have you downloaded UWTerminal and unzipped the files into blutil.py dir?" % compiler)
if not os.path.exists(filepath):
raise RuntimeError("File '%s' not found" % filepath)
print("Compiling %s with %s..." % (filepath, os.path.basename(compiler)))
args = [ compiler, filepath ]
if os.name != 'nt':
args = [ "wine" ] + args
ret = subprocess.call(args, stdin=None, stdout=sys.stdout, stderr=sys.stderr, shell=False)
if ret != 0:
raise RuntimeError("Compilation failed")
print("Compilation success")
def upload(self, filepath):
parts = os.path.splitext(filepath)
if parts[1] != ".uwc": # compiled files have .uwc extension
filepath = "%s.uwc" % (parts[0],)
devicename = get_devicename(filepath)
print("Uploading %s as %s" % (filepath, devicename))
self.writecmd('+DEL "%s" +' % devicename)
self.writecmd('+FOW "%s"' % devicename)
with open(filepath, "rb") as f:
for line in chunks(f, 16):
row = "".join([ "%02x" % x for x in line ])
self.writecmd('+FWRH "%s"' % row)
self.writecmd('+FCL')
print("Upload success")
def run(self, filepath):
devicename = get_devicename(filepath)
self.writecmd('') # check is responding at all
print("Running %s..." % devicename)
self.writecmd('+RUN "%s"' % devicename, expect_response=False)
output = self.port.read(1024)
if len(output):
if len(output) >= 3 and output[-3:] == b'00\r':
if len(output) > 3:
print("Output:\n%s" % output[:-3].decode())
print("Program completed successfully.")
elif len(output) > 4 and output[0:4] == b'\n01\t':
errorcode = str(response[4:].decode())[:-1]
print("Error %s: %s" % (errorcode,get_errordesc(errorcode)))
elif output != b'\n00':
print("Immediate output:\n%s" % output)
else:
print("No immediate output, program probably running...")
def list(self):
print("Listing files...")
output = self.writecmd('+DIR')
print(output)
def delete(self, filename):
filename = get_devicename(filename)
print("Removing %s..." % filename)
self.writecmd('+DEL "%s"' % filename)
print("Deleted.")
def format(self):
print("Formatting BL600...")
self.writecmd('')
self.writecmd('&F 1', expect_response=False)
time.sleep(0.2)
self.port.read(1024) # discard anything
print("Format complete. Reconnecting...")
self.writecmd('')
def chunks(somefile, chunklen):
while True:
chunk = somefile.read(chunklen)
if len(chunk) == 0:
return
yield chunk
def get_devicename(filepath):
""" Given a file path, find an acceptable name on the BL filesystem """
filename = os.path.split(filepath)[1]
filename = filename.split('.')[0]
return re.sub(r'[:*?"<>|]', "", filename)[:24]
def test_wine():
""" Check the wine installation is OK """
try:
with tempfile.TemporaryFile() as blackhole:
ret = subprocess.call(["wine", "--version"], stdin=None, stdout=blackhole, stderr=None, shell=False)
if ret != 0:
raise RuntimeError("Wine returned error code" % ret)
except e:
print("Wine execution failed. %s. Make sure wine is in your path and properly configured" % e)
sys.exit(2)
def get_errordesc(code):
""" Go through file with list of error codes to find description """
blutil_dir = os.path.dirname(sys.argv[0])
with open(os.path.join(blutil_dir, 'codes.csv')) as f:
for line in f:
if str(eval("0x"+code)) in line:
return line.split('"')[1]
break
return "(no description available)"
def main():
if os.name != 'nt':
test_wine()
args = parser.parse_args()
device = BLDevice(args)
# Preload any .sb or .uwc file
if args.run is not None:
split = os.path.splitext(args.run)
if split[1] == ".uwc" or split[1] == ".sb":
args.load = args.run
# Precompile any .sb file
if args.load is not None:
split = os.path.splitext(args.load)
if split[1] == ".sb":
args.compile = args.load
ops = []
if args.compile:
ops += [ "compile" ]
if args.load:
ops += [ "load" ]
if args.run:
ops += [ "run" ]
if (args.load or args.run or args.rm or args.ls or args.format or args.model is None) and not args.no_dtr:
print("Resetting board via DTR...")
device.port.setDTR(False)
time.sleep(0.1)
device.port.setDTR(True)
if args.model is not None:
device.model = args.model.replace(" ", "_")
elif args.compile:
device.detect_model()
if len(ops) > 0:
print("Performing %s for %s..." % (", ".join(ops), sys.argv[-1]))
if args.ls:
device.list()
if args.rm:
device.delete(args.rm)
if args.format:
device.format()
if args.compile:
device.compile(args.compile)
if args.load:
device.upload(args.load)
if args.run:
device.run(args.run)
if __name__ == "__main__":
try:
main()
except RuntimeError as e:
print(e)
sys.exit(2)