extarct s3 data with decompression
This commit is contained in:
parent
933f895a67
commit
5813ecfee3
|
@ -1,18 +1,16 @@
|
|||
#!/usr/bin/python3 -u
|
||||
#!/usr/bin/python2 -u
|
||||
# coding=utf-8
|
||||
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from gi.repository import GLib as glib
|
||||
import gobject
|
||||
import signals
|
||||
import config as cfg
|
||||
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
from pymodbus.client import ModbusSerialClient as Modbus
|
||||
from pymodbus.client.sync import ModbusSerialClient as Modbus
|
||||
from pymodbus.exceptions import ModbusException, ModbusIOException
|
||||
from pymodbus.other_message import ReportSlaveIdRequest
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
|
@ -23,353 +21,334 @@ from python_libs.ie_dbus.dbus_service import DBusService
|
|||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, List, Iterable, NoReturn
|
||||
from typing import Callable, List, Iterable, NoReturn
|
||||
|
||||
|
||||
RESET_REGISTER = 0x2087
|
||||
SETTINGS_SERVICE_PREFIX = 'com.victronenergy.settings'
|
||||
INVERTER_SERVICE_PREFIX = 'com.victronenergy.vebus.'
|
||||
|
||||
|
||||
|
||||
def init_modbus(tty):
|
||||
# type: (str) -> Modbus
|
||||
# type: (str) -> Modbus
|
||||
|
||||
logging.debug('initializing Modbus')
|
||||
logging.debug('initializing Modbus')
|
||||
|
||||
return Modbus(
|
||||
port='/dev/' + tty,
|
||||
method=cfg.MODE,
|
||||
baudrate=cfg.BAUD_RATE,
|
||||
stopbits=cfg.STOP_BITS,
|
||||
bytesize=cfg.BYTE_SIZE,
|
||||
timeout=cfg.TIMEOUT,
|
||||
parity=cfg.PARITY)
|
||||
return Modbus(
|
||||
port='/dev/' + tty,
|
||||
method=cfg.MODE,
|
||||
baudrate=cfg.BAUD_RATE,
|
||||
stopbits=cfg.STOP_BITS,
|
||||
bytesize=cfg.BYTE_SIZE,
|
||||
timeout=cfg.TIMEOUT,
|
||||
parity=cfg.PARITY)
|
||||
|
||||
|
||||
def init_udp_socket():
|
||||
# type: () -> socket
|
||||
# type: () -> socket
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setblocking(False)
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setblocking(False)
|
||||
|
||||
return s
|
||||
return s
|
||||
|
||||
|
||||
def report_slave_id(modbus, slave_address):
|
||||
# type: (Modbus, int) -> str
|
||||
# type: (Modbus, int) -> str
|
||||
|
||||
slave = str(slave_address)
|
||||
slave = str(slave_address)
|
||||
|
||||
logging.debug('requesting slave id from node ' + slave)
|
||||
logging.debug('requesting slave id from node ' + slave)
|
||||
|
||||
with modbus:
|
||||
with modbus:
|
||||
|
||||
request = ReportSlaveIdRequest(unit=slave_address)
|
||||
response = modbus.execute(request)
|
||||
request = ReportSlaveIdRequest(unit=slave_address)
|
||||
response = modbus.execute(request)
|
||||
|
||||
if response is ExceptionResponse or issubclass(type(response), ModbusException):
|
||||
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
|
||||
if response is ExceptionResponse or issubclass(type(response), ModbusException):
|
||||
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
|
||||
|
||||
return response.identifier
|
||||
return response.identifier
|
||||
|
||||
|
||||
def identify_battery(modbus, slave_address):
|
||||
# type: (Modbus, int) -> Battery
|
||||
# type: (Modbus, int) -> Battery
|
||||
|
||||
logging.info('identifying battery...')
|
||||
logging.info('identifying battery...')
|
||||
|
||||
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
|
||||
firmware_version = read_firmware_version(modbus, slave_address)
|
||||
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
|
||||
firmware_version = read_firmware_version(modbus, slave_address)
|
||||
|
||||
specs = Battery(
|
||||
slave_address=slave_address,
|
||||
hardware_version=hardware_version,
|
||||
firmware_version=firmware_version,
|
||||
bms_version=bms_version,
|
||||
ampere_hours=ampere_hours)
|
||||
specs = Battery(
|
||||
slave_address=slave_address,
|
||||
hardware_version=hardware_version,
|
||||
firmware_version=firmware_version,
|
||||
bms_version=bms_version,
|
||||
ampere_hours=ampere_hours)
|
||||
|
||||
logging.info('battery identified:\n{0}'.format(str(specs)))
|
||||
logging.info('battery identified:\n{0}'.format(str(specs)))
|
||||
|
||||
return specs
|
||||
return specs
|
||||
|
||||
|
||||
def identify_batteries(modbus):
|
||||
# type: (Modbus) -> List[Battery]
|
||||
# type: (Modbus) -> List[Battery]
|
||||
|
||||
def _identify_batteries():
|
||||
slave_address = 0
|
||||
n_missing = -255
|
||||
def _identify_batteries():
|
||||
slave_address = 0
|
||||
n_missing = -255
|
||||
|
||||
while n_missing < 3:
|
||||
slave_address += 1
|
||||
try:
|
||||
yield identify_battery(modbus, slave_address)
|
||||
n_missing = 0
|
||||
except Exception as e:
|
||||
logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e)))
|
||||
n_missing += 1
|
||||
while n_missing < 3:
|
||||
slave_address += 1
|
||||
try:
|
||||
yield identify_battery(modbus, slave_address)
|
||||
n_missing = 0
|
||||
except Exception as e:
|
||||
logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e)))
|
||||
n_missing += 1
|
||||
|
||||
logging.info('giving up searching for further batteries')
|
||||
logging.info('giving up searching for further batteries')
|
||||
|
||||
batteries = list(_identify_batteries()) # dont be lazy!
|
||||
batteries = list(_identify_batteries()) # dont be lazy!
|
||||
|
||||
n = len(batteries)
|
||||
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
|
||||
n = len(batteries)
|
||||
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
|
||||
|
||||
return batteries
|
||||
return batteries
|
||||
|
||||
|
||||
def parse_slave_id(modbus, slave_address):
|
||||
# type: (Modbus, int) -> (str, str, int)
|
||||
# type: (Modbus, int) -> (str, str, int)
|
||||
|
||||
slave_id = report_slave_id(modbus, slave_address)
|
||||
slave_id = report_slave_id(modbus, slave_address)
|
||||
|
||||
sid = re.sub(r'[^\x20-\x7E]', '', slave_id) # remove weird special chars
|
||||
sid = re.sub(r'[^\x20-\x7E]', '', slave_id) # remove weird special chars
|
||||
|
||||
match = re.match('(?P<hw>48TL(?P<ah>[0-9]+)) *(?P<bms>.*)', sid)
|
||||
match = re.match('(?P<hw>48TL(?P<ah>[0-9]+)) *(?P<bms>.*)', sid)
|
||||
|
||||
if match is None:
|
||||
raise Exception('no known battery found')
|
||||
if match is None:
|
||||
raise Exception('no known battery found')
|
||||
|
||||
return match.group('hw').strip(), match.group('bms').strip(), int(match.group('ah').strip())
|
||||
return match.group('hw').strip(), match.group('bms').strip(), int(match.group('ah').strip())
|
||||
|
||||
|
||||
def read_firmware_version(modbus, slave_address):
|
||||
# type: (Modbus, int) -> str
|
||||
# type: (Modbus, int) -> str
|
||||
|
||||
logging.debug('reading firmware version')
|
||||
logging.debug('reading firmware version')
|
||||
|
||||
with modbus:
|
||||
with modbus:
|
||||
|
||||
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
|
||||
register = response.registers[0]
|
||||
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
|
||||
register = response.registers[0]
|
||||
|
||||
return '{0:0>4X}'.format(register)
|
||||
return '{0:0>4X}'.format(register)
|
||||
|
||||
|
||||
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
|
||||
# type: (Modbus, int, int, int) -> ReadInputRegistersResponse
|
||||
# type: (Modbus, int, int, int) -> ReadInputRegistersResponse
|
||||
|
||||
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
|
||||
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
|
||||
|
||||
return modbus.read_input_registers(
|
||||
address=base_address,
|
||||
count=count,
|
||||
unit=slave_address)
|
||||
return modbus.read_input_registers(
|
||||
address=base_address,
|
||||
count=count,
|
||||
unit=slave_address)
|
||||
|
||||
|
||||
def read_battery_status(modbus, battery):
|
||||
# type: (Modbus, Battery) -> BatteryStatus
|
||||
"""
|
||||
Read the modbus registers containing the battery's status info.
|
||||
"""
|
||||
# type: (Modbus, Battery) -> BatteryStatus
|
||||
"""
|
||||
Read the modbus registers containing the battery's status info.
|
||||
"""
|
||||
|
||||
logging.debug('reading battery status')
|
||||
logging.debug('reading battery status')
|
||||
|
||||
with modbus:
|
||||
data = read_modbus_registers(modbus, battery.slave_address)
|
||||
return BatteryStatus(battery, data.registers)
|
||||
with modbus:
|
||||
data = read_modbus_registers(modbus, battery.slave_address)
|
||||
return BatteryStatus(battery, data.registers)
|
||||
|
||||
|
||||
def publish_values_on_dbus(service, battery_signals, battery_statuses):
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
|
||||
publish_individuals(service, battery_signals, battery_statuses)
|
||||
publish_aggregates(service, battery_signals, battery_statuses)
|
||||
publish_individuals(service, battery_signals, battery_statuses)
|
||||
publish_aggregates(service, battery_signals, battery_statuses)
|
||||
|
||||
|
||||
def publish_aggregates(service, signals, battery_statuses):
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
|
||||
for s in signals:
|
||||
if s.aggregate is None:
|
||||
continue
|
||||
values = [s.get_value(battery_status) for battery_status in battery_statuses]
|
||||
value = s.aggregate(values)
|
||||
|
||||
service.own_properties.set(s.dbus_path, value, s.unit)
|
||||
for s in signals:
|
||||
if s.aggregate is None:
|
||||
continue
|
||||
values = [s.get_value(battery_status) for battery_status in battery_statuses]
|
||||
value = s.aggregate(values)
|
||||
service.own_properties.set(s.dbus_path, value, s.unit)
|
||||
|
||||
|
||||
def publish_individuals(service, signals, battery_statuses):
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
|
||||
for signal in signals:
|
||||
for battery_status in battery_statuses:
|
||||
address = battery_status.battery.slave_address
|
||||
dbus_path = '/_Battery/' + str(address) + signal.dbus_path
|
||||
value = signal.get_value(battery_status)
|
||||
service.own_properties.set(dbus_path, value, signal.unit)
|
||||
for signal in signals:
|
||||
for battery_status in battery_statuses:
|
||||
address = battery_status.battery.slave_address
|
||||
dbus_path = '/_Battery/' + str(address) + signal.dbus_path
|
||||
value = signal.get_value(battery_status)
|
||||
service.own_properties.set(dbus_path, value, signal.unit)
|
||||
|
||||
|
||||
def publish_service_signals(service, signals):
|
||||
# type: (DBusService, Iterable[ServiceSignal]) -> NoReturn
|
||||
# type: (DBusService, Iterable[ServiceSignal]) -> NoReturn
|
||||
|
||||
for signal in signals:
|
||||
service.own_properties.set(signal.dbus_path, signal.value, signal.unit)
|
||||
for signal in signals:
|
||||
service.own_properties.set(signal.dbus_path, signal.value, signal.unit)
|
||||
|
||||
|
||||
def upload_status_to_innovenergy(sock, statuses):
|
||||
# type: (socket, Iterable[BatteryStatus]) -> bool
|
||||
# type: (socket, Iterable[BatteryStatus]) -> bool
|
||||
|
||||
logging.debug('upload status')
|
||||
logging.debug('upload status')
|
||||
|
||||
try:
|
||||
for s in statuses:
|
||||
sock.sendto(s.serialize(), (cfg.INNOVENERGY_SERVER_IP, cfg.INNOVENERGY_SERVER_PORT))
|
||||
except:
|
||||
logging.debug('FAILED')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
try:
|
||||
for s in statuses:
|
||||
sock.sendto(s.serialize(), (cfg.INNOVENERGY_SERVER_IP, cfg.INNOVENERGY_SERVER_PORT))
|
||||
except:
|
||||
logging.debug('FAILED')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def print_usage():
|
||||
print ('Usage: ' + __file__ + ' <serial device>')
|
||||
print ('Example: ' + __file__ + ' ttyUSB0')
|
||||
print ('Usage: ' + __file__ + ' <serial device>')
|
||||
print ('Example: ' + __file__ + ' ttyUSB0')
|
||||
|
||||
|
||||
def parse_cmdline_args(argv):
|
||||
# type: (List[str]) -> str
|
||||
# type: (List[str]) -> str
|
||||
|
||||
if len(argv) == 0:
|
||||
logging.info('missing command line argument for tty device')
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
if len(argv) == 0:
|
||||
logging.info('missing command line argument for tty device')
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
return argv[0]
|
||||
return argv[0]
|
||||
|
||||
|
||||
def reset_batteries(modbus, batteries):
|
||||
# type: (Modbus, Iterable[Battery]) -> NoReturn
|
||||
# type: (Modbus, Iterable[Battery]) -> NoReturn
|
||||
|
||||
logging.info('Resetting batteries...')
|
||||
logging.info('Resetting batteries...')
|
||||
|
||||
for battery in batteries:
|
||||
for battery in batteries:
|
||||
|
||||
result = modbus.write_registers(RESET_REGISTER, [1], unit=battery.slave_address)
|
||||
result = modbus.write_registers(RESET_REGISTER, [1], unit=battery.slave_address)
|
||||
|
||||
# expecting a ModbusIOException (timeout)
|
||||
# BMS can no longer reply because it is already reset
|
||||
success = isinstance(result, ModbusIOException)
|
||||
# expecting a ModbusIOException (timeout)
|
||||
# BMS can no longer reply because it is already reset
|
||||
success = isinstance(result, ModbusIOException)
|
||||
|
||||
outcome = 'successfully' if success else 'FAILED to'
|
||||
logging.info('Battery {0} {1} reset'.format(str(battery.slave_address), outcome))
|
||||
outcome = 'successfully' if success else 'FAILED to'
|
||||
logging.info('Battery {0} {1} reset'.format(str(battery.slave_address), outcome))
|
||||
|
||||
logging.info('Shutting down fz-sonick driver')
|
||||
exit(0)
|
||||
logging.info('Shutting down fz-sonick driver')
|
||||
exit(0)
|
||||
|
||||
|
||||
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
||||
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
||||
|
||||
|
||||
def create_update_task(modbus, service, batteries):
|
||||
# type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool]
|
||||
"""
|
||||
Creates an update task which runs the main update function
|
||||
and resets the alive flag
|
||||
"""
|
||||
_socket = init_udp_socket()
|
||||
_signals = signals.init_battery_signals()
|
||||
# type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool]
|
||||
"""
|
||||
Creates an update task which runs the main update function
|
||||
and resets the alive flag
|
||||
"""
|
||||
_socket = init_udp_socket()
|
||||
_signals = signals.init_battery_signals()
|
||||
|
||||
def update_task():
|
||||
# type: () -> bool
|
||||
def update_task():
|
||||
# type: () -> bool
|
||||
|
||||
global alive
|
||||
global alive
|
||||
|
||||
logging.debug('starting update cycle')
|
||||
logging.debug('starting update cycle')
|
||||
|
||||
# Checking if we have excess power and if so charge batteries more
|
||||
if service.own_properties.get('/ResetBatteries').value == 1:
|
||||
reset_batteries(modbus, batteries)
|
||||
|
||||
target = service.remote_properties.get(get_service(SETTINGS_SERVICE_PREFIX) + '/Settings/CGwacs/AcPowerSetPoint').value or 0
|
||||
actual = service.remote_properties.get(get_service(INVERTER_SERVICE_PREFIX) + '/Ac/Out/P').value or 0
|
||||
statuses = [read_battery_status(modbus, battery) for battery in batteries]
|
||||
|
||||
if actual>target:
|
||||
service.own_properties.set('/Info/MaxChargeCurrent').value = min([battery.i_max for battery in batteries])
|
||||
publish_values_on_dbus(service, _signals, statuses)
|
||||
upload_status_to_innovenergy(_socket, statuses)
|
||||
|
||||
if service.own_properties.get('/ResetBatteries').value == 1:
|
||||
reset_batteries(modbus, batteries)
|
||||
logging.debug('finished update cycle\n')
|
||||
|
||||
statuses = [read_battery_status(modbus, battery) for battery in batteries]
|
||||
alive = True
|
||||
|
||||
publish_values_on_dbus(service, _signals, statuses)
|
||||
upload_status_to_innovenergy(_socket, statuses)
|
||||
return True
|
||||
|
||||
logging.debug('finished update cycle\n')
|
||||
|
||||
alive = True
|
||||
|
||||
return True
|
||||
|
||||
return update_task
|
||||
return update_task
|
||||
|
||||
|
||||
def create_watchdog_task(main_loop):
|
||||
# type: (DBusGMainLoop) -> Callable[[],bool]
|
||||
"""
|
||||
Creates a Watchdog task that monitors the alive flag.
|
||||
The watchdog kills the main loop if the alive flag is not periodically reset by the update task.
|
||||
Who watches the watchdog?
|
||||
"""
|
||||
# type: (DBusGMainLoop) -> Callable[[],bool]
|
||||
"""
|
||||
Creates a Watchdog task that monitors the alive flag.
|
||||
The watchdog kills the main loop if the alive flag is not periodically reset by the update task.
|
||||
Who watches the watchdog?
|
||||
"""
|
||||
def watchdog_task():
|
||||
# type: () -> bool
|
||||
|
||||
def watchdog_task():
|
||||
# type: () -> bool
|
||||
global alive
|
||||
|
||||
global alive
|
||||
if alive:
|
||||
logging.debug('watchdog_task: update_task is alive')
|
||||
alive = False
|
||||
return True
|
||||
else:
|
||||
logging.info('watchdog_task: killing main loop because update_task is no longer alive')
|
||||
main_loop.quit()
|
||||
return False
|
||||
|
||||
if alive:
|
||||
logging.debug('watchdog_task: update_task is alive')
|
||||
alive = False
|
||||
return True
|
||||
else:
|
||||
logging.info('watchdog_task: killing main loop because update_task is no longer alive')
|
||||
main_loop.quit()
|
||||
return False
|
||||
|
||||
return watchdog_task
|
||||
|
||||
def get_service(self, prefix: str) -> Optional[unicode]:
|
||||
service = next((s for s in self.available_services if s.startswith(prefix)), None)
|
||||
if service is None:
|
||||
raise Exception('no service matching ' + prefix + '* available')
|
||||
|
||||
return service
|
||||
return watchdog_task
|
||||
|
||||
|
||||
def main(argv):
|
||||
# type: (List[str]) -> ()
|
||||
# type: (List[str]) -> ()
|
||||
|
||||
logging.basicConfig(level=cfg.LOG_LEVEL)
|
||||
logging.info('starting ' + __file__)
|
||||
logging.basicConfig(level=cfg.LOG_LEVEL)
|
||||
logging.info('starting ' + __file__)
|
||||
|
||||
tty = parse_cmdline_args(argv)
|
||||
modbus = init_modbus(tty)
|
||||
tty = parse_cmdline_args(argv)
|
||||
modbus = init_modbus(tty)
|
||||
|
||||
batteries = identify_batteries(modbus)
|
||||
batteries = identify_batteries(modbus)
|
||||
|
||||
if len(batteries) <= 0:
|
||||
sys.exit(2)
|
||||
if len(batteries) <= 0:
|
||||
sys.exit(2)
|
||||
|
||||
service = DBusService(service_name=cfg.SERVICE_NAME_PREFIX + tty)
|
||||
service = DBusService(service_name=cfg.SERVICE_NAME_PREFIX + tty)
|
||||
|
||||
service.own_properties.set('/ResetBatteries', value=False, writable=True) # initial value = False
|
||||
service.own_properties.set('/ResetBatteries', value=False, writable=True) # initial value = False
|
||||
|
||||
main_loop = GLib.MainLoop()
|
||||
main_loop = gobject.MainLoop()
|
||||
|
||||
service_signals = signals.init_service_signals(batteries)
|
||||
publish_service_signals(service, service_signals)
|
||||
service_signals = signals.init_service_signals(batteries)
|
||||
publish_service_signals(service, service_signals)
|
||||
|
||||
update_task = create_update_task(modbus, service, batteries)
|
||||
update_task() # run it right away, so that all props are initialized before anyone can ask
|
||||
watchdog_task = create_watchdog_task(main_loop)
|
||||
update_task = create_update_task(modbus, service, batteries)
|
||||
update_task() # run it right away, so that all props are initialized before anyone can ask
|
||||
watchdog_task = create_watchdog_task(main_loop)
|
||||
|
||||
GLib.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task, priority = GLib.PRIORITY_LOW) # add watchdog first
|
||||
GLib.timeout_add(cfg.UPDATE_INTERVAL, update_task, priority = GLib.PRIORITY_LOW) # call update once every update_interval
|
||||
gobject.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task, priority = gobject.PRIORITY_LOW) # add watchdog first
|
||||
gobject.timeout_add(cfg.UPDATE_INTERVAL, update_task, priority = gobject.PRIORITY_LOW) # call update once every update_interval
|
||||
|
||||
logging.info('starting gobject.MainLoop')
|
||||
main_loop.run()
|
||||
logging.info('gobject.MainLoop was shut down')
|
||||
logging.info('starting gobject.MainLoop')
|
||||
main_loop.run()
|
||||
logging.info('gobject.MainLoop was shut down')
|
||||
|
||||
sys.exit(0xFF) # reaches this only on error
|
||||
sys.exit(0xFF) # reaches this only on error
|
||||
|
||||
|
||||
main(sys.argv[1:])
|
||||
|
|
Loading…
Reference in New Issue