├── .gitignore ├── LICENSE ├── README.md ├── archive └── setup.py ├── docs └── images │ ├── extensions.png │ └── robohat_blocks.png ├── esp_8266_micropython ├── __init__.py ├── esp_8266_max.py ├── esp_8266_min.py └── main.py ├── pypi_desc.md ├── pyproject.toml └── s3_extend ├── __init__.py ├── gateways ├── __init__.py ├── arduino_gateway.py ├── cpx_gateway.py ├── esp32_gateway.py ├── esp8266_gateway.py ├── picoboard_gateway.py ├── robohat_gateway.py ├── rpi_gateway.py ├── rpi_pico_gateway.py ├── servo.py ├── sonar.py ├── stepper.py └── ws_gateway.py ├── s32.py ├── s3a.py ├── s3c.py ├── s3e.py ├── s3p.py ├── s3r.py ├── s3rh.py └── s3rp.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scratch 3 OneGPIO Extension Servers 2 | 3 | ![](./docs/images/extensions.png) 4 | 5 | ## Control your favorite physical computing device using Scratch 3. 6 | 7 | ## Windows, Mac *AND* Linux(including Raspberry Pi) Compatible 8 | 9 | ## Quick Intallation Intructions: 10 | 11 | * For Arduino, Circuit Playground Express, ESP-8266, ESP-32, Robohat-MM1, and Raspberry Pi 12 | Pico boards, install the server firmware. See the 13 | [Preparing Your Micro-Controller](https://mryslab.github.io/s3-extend/) section 14 | of the User's Guide. 15 | 16 | * Launch the Scratch3 Editor using either the online or offline sites described 17 | in the [Ready, Set, Go/Launch The Scratch3 Editor](https://mryslab.github.io/s3-extend/) 18 | section of the User's Guide. 19 | 20 | * Select your extension and start coding!. 21 | 22 | ## For full installation and usage, read the [Installation and User's Guide.](https://mryslab.github.io/s3-extend/) 23 | 24 | This project was developed with 25 | [Pycharm](https://www.jetbrains.com/pycharm/?from=s3-extend) 26 | ![logo](https://github.com/MrYsLab/python_banyan/blob/master/images/icon_PyCharm.png) 27 | 28 | 29 | -------------------------------------------------------------------------------- /archive/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('../pypi_desc.md') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='s3-extend', 8 | version='1.30', 9 | packages=[ 10 | 's3_extend', 11 | 's3_extend.gateways' 12 | ], 13 | install_requires=[ 14 | 'python-banyan>=3.10', 15 | 'pymata-express>=1.11', 16 | 'pymata_rh', 17 | 'pymata-cpx', 18 | 'tmx-pico-aio', 19 | 'telemetrix-aio>=1.8', 20 | 'telemetrix-esp32' 21 | ], 22 | 23 | entry_points={ 24 | 'console_scripts': [ 25 | 's3a = s3_extend.s3a:s3ax', 26 | 's3c = s3_extend.s3c:s3cx', 27 | 's3e = s3_extend.s3e:s3ex', 28 | 's32 = s3_extend.s32:s32ex', 29 | 's3p = s3_extend.s3p:s3px', 30 | 's3r = s3_extend.s3r:s3rx', 31 | 's3rh = s3_extend.s3rh:s3rhx', 32 | 's3rp = s3_extend.s3rp:s3rpx', 33 | 'ardgw = s3_extend.gateways.arduino_gateway:arduino_gateway', 34 | 'cpxgw = s3_extend.gateways.cpx_gateway:cpx_gateway', 35 | 'espgw = s3_extend.gateways.esp8266_gateway:esp8266_gateway', 36 | 'esp32gw = s3_extend.gateways.esp32_gateway:esp32_gateway', 37 | 'pbgw = s3_extend.gateways.picoboard_gateway:picoboard_gateway', 38 | 'rpigw = s3_extend.gateways.rpi_gateway:rpi_gateway', 39 | 'rhgw = s3_extend.gateways.robohat_gateway:robohat_gateway', 40 | 'rpgw = s3_extend.gateways.rpi_pico_gateway:rpi_pico_gateway', 41 | 'wsgw = s3_extend.gateways.ws_gateway:ws_gateway', 42 | ] 43 | }, 44 | 45 | url='https://github.com/MrYsLab/s3-extend', 46 | license='GNU Affero General Public License v3 or later (AGPLv3+)', 47 | author='Alan Yorinks', 48 | author_email='MisterYsLab@gmail.com', 49 | description='A Non-Blocking Event Driven Applications Framework', 50 | long_description=long_description, 51 | long_description_content_type='text/markdown', 52 | keywords=['Scratch3', 'Arduino', 'ESP-8266', 'Raspberry Pi'], 53 | classifiers=[ 54 | 'Development Status :: 5 - Production/Stable', 55 | 'Environment :: Other Environment', 56 | 'Intended Audience :: Education', 57 | 'Intended Audience :: Developers', 58 | 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 59 | 'Operating System :: OS Independent', 60 | 'Programming Language :: Python :: 3.7', 61 | 'Topic :: Education', 62 | 'Topic :: Software Development', 63 | 'Topic :: System :: Hardware' 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /docs/images/extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrYsLab/s3-extend/b3a79407251db6d54a19fdb346dbf2886efc365e/docs/images/extensions.png -------------------------------------------------------------------------------- /docs/images/robohat_blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrYsLab/s3-extend/b3a79407251db6d54a19fdb346dbf2886efc365e/docs/images/robohat_blocks.png -------------------------------------------------------------------------------- /esp_8266_micropython/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrYsLab/s3-extend/b3a79407251db6d54a19fdb346dbf2886efc365e/esp_8266_micropython/__init__.py -------------------------------------------------------------------------------- /esp_8266_micropython/esp_8266_max.py: -------------------------------------------------------------------------------- 1 | import uerrno 2 | import ujson 3 | import utime 4 | from machine import Pin, PWM, ADC, I2C, time_pulse_us 5 | from utime import sleep_us 6 | 7 | 8 | class Ultrasonic: 9 | # this code is based from this URL: https://www.stavros.io/tips/ 10 | def __init__(self, t_pin, e_pin): 11 | """ 12 | 13 | :param t_pin: Trigger Pin 14 | :param e_pin: Echo Pin 15 | """ 16 | self.trigger = Pin(t_pin, Pin.OUT) 17 | self.trigger.value(0) 18 | self.echo = Pin(e_pin, Pin.IN) 19 | 20 | def distance_in_cm(self): 21 | self.trigger.value(1) 22 | sleep_us(10) 23 | self.trigger.value(0) 24 | try: 25 | time = time_pulse_us(self.echo, 1, 29000) 26 | except OSError: 27 | return None 28 | dist_in_cm = (time / 2.0) / 29 29 | return dist_in_cm 30 | 31 | 32 | class Esp8266: 33 | """ 34 | This class receives TCP/IP packets and processes 35 | commands to control an ESP8266 and sends reports 36 | over the TCP/IP link. 37 | """ 38 | def __init__(self, socket, packet_len=96): 39 | """ 40 | :param socket: connected TCP/IP socket - see main.py 41 | :param packet_len: A fixed packet length 42 | """ 43 | # save the socket and packet length parameters 44 | self.socket = socket 45 | self.packet_len = packet_len 46 | 47 | # previous adc value 48 | self.last_adc = 0 49 | 50 | # differential between previous and current adc value 51 | # to trigger a report 52 | self.adc_diff_report = 0 53 | 54 | # adc object 55 | self.adc = None 56 | 57 | # i2c object 58 | self.i2c = None 59 | self.i2c_continuous = False 60 | 61 | # sonar object 62 | self.sonar = None 63 | 64 | # supported GPIO pins 65 | self.gpio_pins = [4, 5, 12, 13, 14, 15] 66 | 67 | # list of stepper motor pins 68 | self.stepper_pins = [] 69 | 70 | # a list of pin objects to be used when reporting 71 | # back digital input pin changes 72 | self.input_pin_objects = [None] * 16 73 | 74 | # incoming command lookup table 75 | self.command_dictionary = {'analog_write': self.analog_write, 'digital_write': self.digital_write, 76 | 'disable_analog_reporting': self.disable_analog_reporting, 77 | 'disable_digital_reporting': self.disable_digital_reporting, 78 | 'enable_analog_reporting': self.disable_analog_reporting, 79 | 'enable_digital_reporting': self.disable_digital_reporting, 80 | 'i2c_read': self.i2c_read, 'i2c_write': self.i2c_write, 'play_tone': self.play_tone, 81 | 'pwm_write': self.pwm_write, 'servo_position': self.servo_position, 82 | 'set_mode_analog_input': self.set_mode_analog_input, 83 | 'set_mode_digital_input': self.set_mode_digital_input, 84 | 'set_mode_digital_input_pullup': self.set_mode_digital_input_pullup, 85 | 'set_mode_digital_output': self.set_mode_digital_output, 86 | 'set_mode_i2c': self.set_mode_i2c, 'set_mode_pwm': self.set_mode_pwm, 87 | 'set_mode_servo': self.set_mode_servo, 'set_mode_sonar': self.set_mode_sonar, 88 | 'set_mode_stepper': self.set_mode_stepper, 'set_mode_tone': self.set_mode_tone, 89 | 'stepper_write': self.stepper_write, } 90 | 91 | # digital input change callback message 92 | self.digital_input_cb_msg = {'report': 'digital_input', 'pin': 0, 'value': 0} 93 | 94 | # set the socket to non-blocking 95 | self.socket.setblocking(False) 96 | 97 | # output an id string in case we need it 98 | info = 'esp connected to ' + repr(self.socket) 99 | self.send_payload_to_gateway({'report': 'connected', 'info': info}) 100 | 101 | # start the command processing loop 102 | self.get_next_command() 103 | 104 | def get_next_command(self): 105 | """ 106 | Command processing loop 107 | """ 108 | while True: 109 | try: 110 | # wait for all data to arrive for a packet 111 | # of packet_len 112 | payload = self.socket.recv(self.packet_len) 113 | pkt_len_received = len(payload) 114 | print(pkt_len_received) 115 | while pkt_len_received < self.packet_len: 116 | wait_for = self.packet_len - pkt_len_received 117 | short_packet = self.socket.recv(wait_for) 118 | payload += short_packet 119 | pkt_len_received += len(payload) 120 | except OSError: 121 | # this exception is expected because we set the 122 | # socket to non-blocking. 123 | 124 | # if we have an adc object, get the latest value and send report 125 | if self.adc: 126 | adc_val = self.adc.read() 127 | if abs(self.last_adc - adc_val) >= self.adc_diff_report: 128 | self.send_payload_to_gateway({'report': 'analog_input', 'pin': 0, 'value': adc_val}) 129 | self.last_adc = adc_val 130 | # if sonar is enabled get next sample 131 | if self.sonar: 132 | dist = self.sonar.distance_in_cm() 133 | payload = {'report': 'sonar_data', 'value': dist} 134 | self.send_payload_to_gateway(payload) 135 | utime.sleep(.05) 136 | continue 137 | try: 138 | # decode the payload 139 | payload = ujson.loads(payload) 140 | except ValueError: 141 | self.send_payload_to_gateway( 142 | {'error': 'json value error - data: {} length {}'.format(payload, len(payload))}) 143 | self.socket.close() 144 | break 145 | 146 | # get the payload command and look it up in the 147 | # command dictionary to execute the command processing 148 | command = payload['command'] 149 | if command in self.command_dictionary.keys(): 150 | self.command_dictionary[command](payload) 151 | else: 152 | self.additional_banyan_messages(payload) 153 | 154 | def additional_banyan_messages(self, payload): 155 | """ 156 | For future development 157 | :param payload: 158 | :return: 159 | """ 160 | raise NotImplementedError 161 | 162 | def analog_write(self, payload): 163 | """ 164 | Not implemented - placeholder 165 | :param payload: 166 | :return: 167 | """ 168 | raise NotImplementedError 169 | 170 | def digital_write(self, payload): 171 | """ 172 | Write to a digital gpio pin 173 | :param payload: 174 | :return: 175 | """ 176 | pin = payload['pin'] 177 | mode = Pin.OUT 178 | if 'drain' in payload: 179 | if not payload['drain']: 180 | mode = Pin.OPEN_DRAIN 181 | pin_object = Pin(pin, mode) 182 | pwm = PWM(pin_object) 183 | pwm.deinit() 184 | Pin(pin, mode, value=payload['value']) 185 | self.input_pin_objects[pin] = None 186 | 187 | def disable_analog_reporting(self, payload): 188 | """ 189 | disable analog reporting 190 | :param payload: 191 | :return: 192 | """ 193 | self.adc = None 194 | 195 | def disable_digital_reporting(self, payload): 196 | """ 197 | disable digital reporting for the pin 198 | :param payload: 199 | :return: 200 | """ 201 | self.input_pin_objects[payload['pin']] = None 202 | 203 | def enable_analog_reporting(self, payload): 204 | """ 205 | Will be auto enabled when set_mode_analog_input 206 | is called. 207 | :param payload: 208 | :return: 209 | """ 210 | raise NotImplementedError 211 | 212 | def enable_digital_reporting(self, payload): 213 | """ 214 | Will be auto enabled when set_mode_digital_input is called 215 | :param payload: 216 | :return: 217 | """ 218 | raise NotImplementedError 219 | 220 | def i2c_read(self, payload): 221 | """ 222 | Establish an i2c object if not already establed and 223 | read from the device. 224 | :param payload: 225 | :return: 226 | """ 227 | if not self.i2c: 228 | self.i2c = I2C(scl=Pin(5), sda=Pin(4), freq=100000) 229 | try: 230 | data = self.i2c.readfrom_mem(payload['addr'], payload['register'], payload['number_of_bytes']) 231 | except TypeError: 232 | print('read') 233 | raise 234 | try: 235 | data = list(data) 236 | except TypeError: 237 | print(payload, data) 238 | raise 239 | payload = {'report': 'i2c_data', 'value': data} 240 | self.send_payload_to_gateway(payload) 241 | 242 | def i2c_write(self, payload): 243 | """ 244 | Establish an i2c object if not already establed and 245 | write to the device. 246 | :param payload: 247 | :return: 248 | """ 249 | if not self.i2c: 250 | self.i2c = I2C(scl=Pin(5), sda=Pin(4), freq=100000) 251 | self.i2c.writeto(payload['addr'], bytearray(payload['data'])) 252 | 253 | def play_tone(self, payload): 254 | """ 255 | Play a tone on the specified pin 256 | :param payload: 257 | :return: 258 | """ 259 | beeper = PWM(Pin(payload['pin']), freq=payload['freq'], duty=512) 260 | utime.sleep_ms(payload['duration']) 261 | beeper.deinit() 262 | 263 | def pwm_write(self, payload): 264 | """ 265 | Write a value to a PWM pin 266 | :param payload: 267 | :return: 268 | """ 269 | PWM(Pin(payload['pin']), freq=500, duty=payload['value']) 270 | 271 | def servo_position(self, payload): 272 | """ 273 | Set the servo position in degrees 274 | :param payload: 275 | :return: 276 | """ 277 | position = 30 + int((payload['position'] * 0.5)) 278 | servo = PWM(Pin(payload['pin']), freq=50, duty=position) 279 | utime.sleep_ms(300) 280 | servo.deinit() 281 | 282 | def set_mode_analog_input(self, payload): 283 | """ 284 | create an adc object 285 | :param payload: 286 | :return: 287 | """ 288 | 289 | if 'change_diff' in payload: 290 | self.adc_diff_report = payload['change_diff'] 291 | self.adc = ADC(0) 292 | 293 | def digital_input_callback(self, p): 294 | """ 295 | digital input pins use irqs. This is the callback 296 | method to send the value change to the tcp/ip client. 297 | :param p: Pin 298 | :return: 299 | """ 300 | # find the Pin object to retrieve the data 301 | for index, item in enumerate(self.input_pin_objects): 302 | if p == item: 303 | self.digital_input_cb_msg['pin'] = index 304 | self.digital_input_cb_msg['value'] = p.value() 305 | self.send_payload_to_gateway(self.digital_input_cb_msg) 306 | 307 | def set_mode_digital_input(self, payload): 308 | """ 309 | Set a pin as a digital input with pull_up and 310 | enable interrupts for a change on either edge. 311 | :param payload: 312 | :return: 313 | """ 314 | pin = payload['pin'] 315 | pin_in = Pin(pin, Pin.IN, Pin.PULL_UP) 316 | pin_in.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.digital_input_callback) 317 | self.input_pin_objects[pin] = pin_in 318 | 319 | def set_mode_digital_input_pullup(self, payload): 320 | """ 321 | Place holder 322 | :param payload: 323 | :return: 324 | """ 325 | raise NotImplementedError 326 | 327 | def set_mode_digital_output(self, payload): 328 | """ 329 | Set pin as a digtal output 330 | :param payload: 331 | :return: 332 | """ 333 | pin = payload['pin'] 334 | mode = Pin.OUT 335 | if 'drain' in payload: 336 | if not payload['drain']: 337 | mode = Pin.OPEN_DRAIN 338 | Pin(pin, mode, value=payload['value']) 339 | 340 | def set_mode_i2c(self, payload): 341 | """ 342 | Placeholder 343 | :param payload: 344 | :return: 345 | """ 346 | pass 347 | 348 | def set_mode_pwm(self, payload): 349 | """ 350 | Placeholder 351 | :param payload: 352 | :return: 353 | """ 354 | raise NotImplementedError 355 | 356 | def set_mode_servo(self, payload): 357 | """ 358 | Placeholder 359 | :param payload: 360 | :return: 361 | """ 362 | pass 363 | 364 | def set_mode_sonar(self, payload): 365 | """ 366 | Establish a sonar object. 367 | Readings are performed in get_next_command 368 | :param payload: 369 | :return: 370 | """ 371 | if not self.sonar: 372 | self.sonar = Ultrasonic(payload['trigger'], payload['echo']) 373 | 374 | def set_mode_stepper(self, payload): 375 | """ 376 | Get the list of stepper pins 377 | :param payload: 378 | :return: 379 | """ 380 | self.stepper_pins = payload['pins'] 381 | 382 | def set_mode_tone(self, payload): 383 | """ 384 | Placeholder 385 | :param payload: 386 | :return: 387 | """ 388 | pass 389 | 390 | def digital_read(self, payload): 391 | """ 392 | Placeholder - irqs automatically report 393 | :param payload: 394 | :return: 395 | """ 396 | raise NotImplementedError 397 | 398 | def stepper_write(self, payload): 399 | """ 400 | Move stepper specified number of steps 401 | :param payload: 402 | :return: 403 | """ 404 | d1 = Pin(self.stepper_pins[0], Pin.OUT) 405 | d2 = Pin(self.stepper_pins[1], Pin.OUT) 406 | d3 = Pin(self.stepper_pins[2], Pin.OUT) 407 | d4 = Pin(self.stepper_pins[3], Pin.OUT) 408 | number_of_steps = payload['number_of_steps'] 409 | if number_of_steps > 0: 410 | for i in range(abs(number_of_steps)): 411 | d1.value(1) 412 | utime.sleep_ms(5) 413 | d1.value(0) 414 | d2.value(1) 415 | utime.sleep_ms(5) 416 | d2.value(0) 417 | d3.value(1) 418 | utime.sleep_ms(5) 419 | d3.value(0) 420 | d4.value(1) 421 | i += 1 422 | utime.sleep_ms(5) 423 | d4.value(0) 424 | else: 425 | for i in range(abs(number_of_steps)): 426 | d4.value(1) 427 | utime.sleep_ms(5) 428 | d4.value(0) 429 | d3.value(1) 430 | utime.sleep_ms(5) 431 | d3.value(0) 432 | d2.value(1) 433 | utime.sleep_ms(5) 434 | d2.value(0) 435 | d1.value(1) 436 | i += 1 437 | utime.sleep_ms(5) 438 | d1.value(0) 439 | 440 | def send_payload_to_gateway(self, payload): 441 | """ 442 | send a payload back to the gateway. 443 | :param payload: 444 | :return: 445 | """ 446 | payload = ujson.dumps(payload) 447 | payload = '{:96}'.format(payload).encode('utf-8') 448 | try: 449 | self.socket.sendall(payload) 450 | except OSError as exc: 451 | if exc.args[0] == uerrno.EAGAIN: 452 | pass 453 | -------------------------------------------------------------------------------- /esp_8266_micropython/esp_8266_min.py: -------------------------------------------------------------------------------- 1 | import uerrno 2 | import ujson 3 | import utime 4 | from machine import Pin,PWM,ADC,I2C,time_pulse_us 5 | from utime import sleep_us 6 | class Ultrasonic: 7 | def __init__(self,t_pin,e_pin): 8 | self.trigger=Pin(t_pin,Pin.OUT) 9 | self.trigger.value(0) 10 | self.echo=Pin(e_pin,Pin.IN) 11 | def distance_in_cm(self): 12 | self.trigger.value(1) 13 | sleep_us(10) 14 | self.trigger.value(0) 15 | try: 16 | time=time_pulse_us(self.echo,1,29000) 17 | except OSError: 18 | return None 19 | dist_in_cm=(time/2.0)/29 20 | return dist_in_cm 21 | class Esp8266: 22 | def __init__(self,socket,packet_len=96): 23 | self.socket=socket 24 | self.packet_len=packet_len 25 | self.last_adc=0 26 | self.adc_diff_report=0 27 | self.adc=None 28 | self.i2c=None 29 | self.i2c_continuous=False 30 | self.sonar=None 31 | self.gpio_pins=[4,5,12,13,14,15] 32 | self.stepper_pins=[] 33 | self.input_pin_objects=[None]*16 34 | self.command_dictionary={'analog_write':self.analog_write,'digital_write':self.digital_write,'disable_analog_reporting':self.disable_analog_reporting,'disable_digital_reporting':self.disable_digital_reporting,'enable_analog_reporting':self.disable_analog_reporting,'enable_digital_reporting':self.disable_digital_reporting,'i2c_read':self.i2c_read,'i2c_write':self.i2c_write,'play_tone':self.play_tone,'pwm_write':self.pwm_write,'servo_position':self.servo_position,'set_mode_analog_input':self.set_mode_analog_input,'set_mode_digital_input':self.set_mode_digital_input,'set_mode_digital_input_pullup':self.set_mode_digital_input_pullup,'set_mode_digital_output':self.set_mode_digital_output,'set_mode_i2c':self.set_mode_i2c,'set_mode_pwm':self.set_mode_pwm,'set_mode_servo':self.set_mode_servo,'set_mode_sonar':self.set_mode_sonar,'set_mode_stepper':self.set_mode_stepper,'set_mode_tone':self.set_mode_tone,'stepper_write':self.stepper_write,} 35 | self.digital_input_cb_msg={'report':'digital_input','pin':0,'value':0} 36 | self.socket.setblocking(False) 37 | info='esp connected to '+repr(self.socket) 38 | self.send_payload_to_gateway({'report':'connected','info':info}) 39 | self.get_next_command() 40 | def get_next_command(self): 41 | while True: 42 | try: 43 | payload=self.socket.recv(self.packet_len) 44 | pkt_len_received=len(payload) 45 | print(pkt_len_received) 46 | while pkt_len_received=self.adc_diff_report: 55 | self.send_payload_to_gateway({'report':'analog_input','pin':0,'value':adc_val}) 56 | self.last_adc=adc_val 57 | if self.sonar: 58 | dist=self.sonar.distance_in_cm() 59 | payload={'report':'sonar_data','value':dist} 60 | self.send_payload_to_gateway(payload) 61 | utime.sleep(.05) 62 | continue 63 | try: 64 | payload=ujson.loads(payload) 65 | except ValueError: 66 | self.send_payload_to_gateway({'error':'json value error - data: {} length {}'.format(payload,len(payload))}) 67 | self.socket.close() 68 | break 69 | command=payload['command'] 70 | if command in self.command_dictionary.keys(): 71 | self.command_dictionary[command](payload) 72 | else: 73 | self.additional_banyan_messages(payload) 74 | def additional_banyan_messages(self,payload): 75 | raise NotImplementedError 76 | def analog_write(self,payload): 77 | raise NotImplementedError 78 | def digital_write(self,payload): 79 | pin=payload['pin'] 80 | mode=Pin.OUT 81 | if 'drain' in payload: 82 | if not payload['drain']: 83 | mode=Pin.OPEN_DRAIN 84 | pin_object=Pin(pin,mode) 85 | pwm=PWM(pin_object) 86 | pwm.deinit() 87 | Pin(pin,mode,value=payload['value']) 88 | self.input_pin_objects[pin]=None 89 | def disable_analog_reporting(self,payload): 90 | self.adc=None 91 | def disable_digital_reporting(self,payload): 92 | self.input_pin_objects[payload['pin']]=None 93 | def enable_analog_reporting(self,payload): 94 | raise NotImplementedError 95 | def enable_digital_reporting(self,payload): 96 | raise NotImplementedError 97 | def i2c_read(self,payload): 98 | if not self.i2c: 99 | self.i2c=I2C(scl=Pin(5),sda=Pin(4),freq=100000) 100 | try: 101 | data=self.i2c.readfrom_mem(payload['addr'],payload['register'],payload['number_of_bytes']) 102 | except TypeError: 103 | print('read') 104 | raise 105 | try: 106 | data=list(data) 107 | except TypeError: 108 | print(payload,data) 109 | raise 110 | payload={'report':'i2c_data','value':data} 111 | self.send_payload_to_gateway(payload) 112 | def i2c_write(self,payload): 113 | if not self.i2c: 114 | self.i2c=I2C(scl=Pin(5),sda=Pin(4),freq=100000) 115 | self.i2c.writeto(payload['addr'],bytearray(payload['data'])) 116 | def play_tone(self,payload): 117 | beeper=PWM(Pin(payload['pin']),freq=payload['freq'],duty=512) 118 | utime.sleep_ms(payload['duration']) 119 | beeper.deinit() 120 | def pwm_write(self,payload): 121 | PWM(Pin(payload['pin']),freq=500,duty=payload['value']) 122 | def servo_position(self,payload): 123 | position=30+int((payload['position']*0.5)) 124 | servo=PWM(Pin(payload['pin']),freq=50,duty=position) 125 | utime.sleep_ms(300) 126 | servo.deinit() 127 | def set_mode_analog_input(self,payload): 128 | if 'change_diff' in payload: 129 | self.adc_diff_report=payload['change_diff'] 130 | self.adc=ADC(0) 131 | def digital_input_callback(self,p): 132 | print(p) 133 | for index,item in enumerate(self.input_pin_objects): 134 | if p==item: 135 | self.digital_input_cb_msg['pin']=index 136 | self.digital_input_cb_msg['value']=p.value() 137 | self.send_payload_to_gateway(self.digital_input_cb_msg) 138 | def set_mode_digital_input(self,payload): 139 | pin=payload['pin'] 140 | pin_in=Pin(pin,Pin.IN,Pin.PULL_UP) 141 | pin_in.irq(trigger=Pin.IRQ_RISING|Pin.IRQ_FALLING,handler=self.digital_input_callback) 142 | self.input_pin_objects[pin]=pin_in 143 | def set_mode_digital_input_pullup(self,payload): 144 | raise NotImplementedError 145 | def set_mode_digital_output(self,payload): 146 | pin=payload['pin'] 147 | mode=Pin.OUT 148 | if 'drain' in payload: 149 | if not payload['drain']: 150 | mode=Pin.OPEN_DRAIN 151 | Pin(pin,mode,value=payload['value']) 152 | def set_mode_i2c(self,payload): 153 | pass 154 | def set_mode_pwm(self,payload): 155 | raise NotImplementedError 156 | def set_mode_servo(self,payload): 157 | pass 158 | def set_mode_sonar(self,payload): 159 | if not self.sonar: 160 | self.sonar=Ultrasonic(payload['trigger'],payload['echo']) 161 | def set_mode_stepper(self,payload): 162 | self.stepper_pins=payload['pins'] 163 | def set_mode_tone(self,payload): 164 | pass 165 | def digital_read(self,payload): 166 | raise NotImplementedError 167 | def stepper_write(self,payload): 168 | d1=Pin(self.stepper_pins[0],Pin.OUT) 169 | d2=Pin(self.stepper_pins[1],Pin.OUT) 170 | d3=Pin(self.stepper_pins[2],Pin.OUT) 171 | d4=Pin(self.stepper_pins[3],Pin.OUT) 172 | number_of_steps=payload['number_of_steps'] 173 | if number_of_steps>0: 174 | for i in range(abs(number_of_steps)): 175 | d1.value(1) 176 | utime.sleep_ms(5) 177 | d1.value(0) 178 | d2.value(1) 179 | utime.sleep_ms(5) 180 | d2.value(0) 181 | d3.value(1) 182 | utime.sleep_ms(5) 183 | d3.value(0) 184 | d4.value(1) 185 | i+=1 186 | utime.sleep_ms(5) 187 | d4.value(0) 188 | else: 189 | for i in range(abs(number_of_steps)): 190 | d4.value(1) 191 | utime.sleep_ms(5) 192 | d4.value(0) 193 | d3.value(1) 194 | utime.sleep_ms(5) 195 | d3.value(0) 196 | d2.value(1) 197 | utime.sleep_ms(5) 198 | d2.value(0) 199 | d1.value(1) 200 | i+=1 201 | utime.sleep_ms(5) 202 | d1.value(0) 203 | def send_payload_to_gateway(self,payload): 204 | payload=ujson.dumps(payload) 205 | payload='{:96}'.format(payload).encode('utf-8') 206 | try: 207 | self.socket.sendall(payload) 208 | except OSError as exc: 209 | if exc.args[0]==uerrno.EAGAIN: 210 | pass 211 | # Created by pyminifier (https://github.com/liftoff/pyminifier) 212 | -------------------------------------------------------------------------------- /esp_8266_micropython/main.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import utime 3 | 4 | from esp_8266_min import Esp8266 5 | 6 | from machine import Pin 7 | 8 | 9 | class Esp8266TcpServer: 10 | """ 11 | This is an on-board esp8266 tcp server 12 | """ 13 | 14 | def __init__(self): 15 | # allow time to connect serial monitor 16 | # uncomment next line for debugging 17 | # utime.sleep(10) 18 | 19 | # set pin that Red LED is connected to output 20 | p = Pin(16, Pin.OUT) 21 | 22 | print('turning led on') 23 | 24 | # blink Red LED 3 times to indicate this 25 | # is a server (morse code 'S') 26 | for x in range(3): 27 | p.value(0) 28 | utime.sleep_ms(50) 29 | p.value(1) 30 | utime.sleep_ms(50) 31 | p.value(0) 32 | utime.sleep_ms(50) 33 | 34 | # leave it lit until connection is established 35 | 36 | # allow any IP address on LAN to connect using port 31337 37 | addr = socket.getaddrinfo('0.0.0.0', 31337)[0][-1] 38 | print(addr) 39 | s = socket.socket() 40 | s.bind(addr) 41 | 42 | # wait for connection request - allow only one connection 43 | s.listen(1) 44 | 45 | # wait to accept connection request 46 | connect_socket, addr = s.accept() 47 | 48 | # set the 49 | # turn off Red LED 50 | p.value(1) 51 | print(connect_socket) 52 | 53 | # start the data processing script 54 | # pass in the connection socket as a result of 55 | # client connecting to this server 56 | Esp8266(connect_socket) 57 | 58 | 59 | # create an instance of the server 60 | Esp8266TcpServer() 61 | -------------------------------------------------------------------------------- /pypi_desc.md: -------------------------------------------------------------------------------- 1 | # Scratch 3 OneGPIO Extension Servers 2 | 3 | ## Control your favorite physical computing device using Scratch 3. 4 | 5 | Quick Intallation Intructions: 6 | 7 | * For Arduino, Circuit Playground Express, ESP-8266, ESP-32, Robohat-MM1, and Raspberry Pi 8 | Pico boards, install the server firmware. See the 9 | [Preparing Your Micro-Controller](https://mryslab.github.io/s3-extend/) section 10 | of the User's Guide. 11 | 12 | * Launch the Scratch3 Editor using either the online or offline sites described 13 | in the in the [Ready, Set, Go/Launch The Scratch3 Editor](https://mryslab.github.io/s3-extend/) section of the User's Guide. 14 | 15 | * Select your extension and start coding!. 16 | 17 | ## For full installation and usage, read the [Installation and User's Guide.](https://mryslab.github.io/s3-extend/) 18 | 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools.packages] 6 | find = { } # Scan the project directory with the default parameters 7 | 8 | [project] 9 | name = "s3-extend" 10 | version = "1.33" 11 | 12 | authors = [ 13 | { name = "Alan Yorinks", email = "MisterYsLab@gmail.com" }, 14 | ] 15 | 16 | description = "Scratch3 Extension Servers" 17 | readme = "README.md" 18 | requires-python = ">=3.8" 19 | license = { text = "AGPL-3.0-or-later" } 20 | 21 | keywords = ['Scratch', ' Extensions', 'Python'] 22 | classifiers = [ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Environment :: Other Environment', 25 | 'Intended Audience :: Developers', 26 | 'Intended Audience :: Education', 27 | 'Operating System :: OS Independent', 28 | 'Topic :: Software Development :: Libraries :: Python Modules' 29 | ] 30 | 31 | dependencies = [ 32 | 'python-banyan>=3.10', 33 | 'pymata-express>=1.11', 34 | 'pymata-rh', 35 | 'pymata-cpx', 36 | 'tmx-pico-aio', 37 | 'telemetrix-aio>=1.8', 38 | 'telemetrix-esp32' 39 | ] 40 | 41 | [project.scripts] 42 | s3a = 's3_extend.s3a:s3ax' 43 | s3c = 's3_extend.s3c:s3cx' 44 | s3e = 's3_extend.s3e:s3ex' 45 | s32 = 's3_extend.s32:s32ex' 46 | s3p = 's3_extend.s3p:s3px' 47 | s3r = 's3_extend.s3r:s3rx' 48 | s3rh = 's3_extend.s3rh:s3rhx' 49 | s3rp = 's3_extend.s3rp:s3rpx' 50 | ardgw = 's3_extend.gateways.arduino_gateway:arduino_gateway' 51 | cpxgw = 's3_extend.gateways.cpx_gateway:cpx_gateway' 52 | espgw = 's3_extend.gateways.esp8266_gateway:esp8266_gateway' 53 | esp32gw = 's3_extend.gateways.esp32_gateway:esp32_gateway' 54 | pbgw = 's3_extend.gateways.picoboard_gateway:picoboard_gateway' 55 | rpigw = 's3_extend.gateways.rpi_gateway:rpi_gateway' 56 | rhgw = 's3_extend.gateways.robohat_gateway:robohat_gateway' 57 | rpgw = 's3_extend.gateways.rpi_pico_gateway:rpi_pico_gateway' 58 | wsgw = 's3_extend.gateways.ws_gateway:ws_gateway' 59 | 60 | 61 | -------------------------------------------------------------------------------- /s3_extend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrYsLab/s3-extend/b3a79407251db6d54a19fdb346dbf2886efc365e/s3_extend/__init__.py -------------------------------------------------------------------------------- /s3_extend/gateways/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrYsLab/s3-extend/b3a79407251db6d54a19fdb346dbf2886efc365e/s3_extend/gateways/__init__.py -------------------------------------------------------------------------------- /s3_extend/gateways/cpx_gateway.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the Python Banyan Hardware Gateway for the 3 | Adafruit Circuit Playground Express. 4 | 5 | Copyright (c) 2020 Alan Yorinks All right reserved. 6 | 7 | Python Banyan is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 9 | Version 3 as published by the Free Software Foundation; either 10 | or (at your option) any later version. 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 17 | along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | """ 21 | import argparse 22 | import atexit 23 | import logging 24 | import math 25 | import os 26 | import pathlib 27 | import signal 28 | import sys 29 | import threading 30 | import time 31 | 32 | import serial 33 | from pymata_cpx.pymata_cpx import PyMataCpx 34 | from python_banyan.gateway_base import GatewayBase 35 | 36 | 37 | # noinspection PyMethodMayBeStatic 38 | class CpxGateway(GatewayBase, threading.Thread): 39 | """ 40 | This class is the interface class for the Circuit Playground 41 | Express supporting Scratch 3. 42 | """ 43 | 44 | def __init__(self, *subscriber_list, back_plane_ip_address=None, subscriber_port='43125', 45 | publisher_port='43124', process_name='CpxGateway', 46 | publisher_topic=None, log=False): 47 | """ 48 | :param subscriber_list: a tuple or list of subscription topics. 49 | :param back_plane_ip_address: 50 | :param subscriber_port: 51 | :param publisher_port: 52 | :param process_name: 53 | """ 54 | 55 | # initialize parent 56 | super(CpxGateway, self).__init__(subscriber_list=subscriber_list, 57 | back_plane_ip_address=back_plane_ip_address, 58 | subscriber_port=subscriber_port, 59 | publisher_port=publisher_port, process_name=process_name) 60 | self.log = log 61 | if self.log: 62 | fn = str(pathlib.Path.home()) + "/cpxgw.log" 63 | self.logger = logging.getLogger(__name__) 64 | logging.basicConfig(filename=fn, filemode='w', level=logging.DEBUG) 65 | sys.excepthook = self.my_handler 66 | 67 | self.publisher_topic = publisher_topic 68 | self.cpx = PyMataCpx() 69 | atexit.register(self.shutdown) 70 | 71 | # hold the time of the last analog data to be received. 72 | # use to determine if connectivity is gone. 73 | self.last_analog_data_time = None 74 | 75 | # start up all the sensors 76 | self.cpx.cpx_accel_start(self.tilt_callback) 77 | self.cpx.cpx_button_a_start(self.switch_callback) 78 | self.cpx.cpx_button_b_start(self.switch_callback) 79 | self.cpx.cpx_slide_switch_start(self.switch_callback) 80 | 81 | self.cpx.cpx_light_sensor_start(self.analog_callback) 82 | self.cpx.cpx_microphone_start(self.analog_callback) 83 | self.cpx.cpx_temperature_start(self.analog_callback) 84 | for touch_pad in range(1, 8): 85 | self.cpx.cpx_cap_touch_start(touch_pad, self.touchpad_callback) 86 | 87 | threading.Thread.__init__(self) 88 | self.daemon = True 89 | 90 | # start the watchdog thread 91 | self.start() 92 | # start the banyan receive loop 93 | try: 94 | self.receive_loop() 95 | except: 96 | pass 97 | # except KeyboardInterrupt: 98 | # except KeyboardInterrupt: 99 | # self.cpx.cpx_close_and_exit() 100 | # sys.exit(0) 101 | # os._exit(1) 102 | 103 | 104 | def init_pins_dictionary(self): 105 | pass 106 | 107 | def play_tone(self, topic, payload): 108 | """ 109 | This method plays a tone on a piezo device connected to the selected 110 | pin at the frequency and duration requested. 111 | Frequency is in hz and duration in milliseconds. 112 | 113 | Call set_mode_tone before using this method. 114 | :param topic: message topic 115 | :param payload: {"command": "play_tone", "pin": “PIN”, "tag": "TAG", 116 | “freq”: ”FREQUENCY”, duration: “DURATION”} 117 | """ 118 | self.cpx.cpx_tone(payload['freq'], payload['duration']) 119 | 120 | def additional_banyan_messages(self, topic, payload): 121 | if payload['command'] == 'pixel': 122 | self.set_pixel(payload) 123 | 124 | def set_pixel(self, payload): 125 | self.cpx.cpx_pixel_set(payload['pixel'], payload['red'], 126 | payload['green'], payload['blue']) 127 | self.cpx.cpx_pixels_show() 128 | 129 | def digital_write(self, topic, payload): 130 | """ 131 | This method performs a digital write to the board LED 132 | :param topic: message topic 133 | :param payload: {"command": "digital_write", "pin": “PIN”, "value": “VALUE”} 134 | """ 135 | if payload['value']: 136 | self.cpx.cpx_board_light_on() 137 | else: 138 | self.cpx.cpx_board_light_off() 139 | 140 | # The CPX sensor callbacks 141 | 142 | def tilt_callback(self, data): 143 | """ 144 | Report the tilt of the express board 145 | 146 | Take the raw xyz data and transform it to 147 | positional strings. 148 | :param data: data [0] = data mode 32 is analog. 149 | data[1] = the pin number - this is a pseudo pin number 150 | data[2] = x value 151 | data[3] = y value 152 | data[4] = z value 153 | """ 154 | x = data[2] 155 | y = data[3] 156 | z = data[4] 157 | 158 | # Convert raw Accelerometer values to degrees 159 | x_angle = int((math.atan2(y, z) + math.pi) * (180 / math.pi)) 160 | y_angle = int((math.atan2(z, x) + math.pi) * (180 / math.pi)) 161 | 162 | h = v = -1 163 | 164 | if 175 < x_angle < 185 and 265 < y_angle < 275: 165 | h = v = 0 # 'flat' 166 | 167 | elif h or v: 168 | if 180 <= x_angle <= 270: 169 | v = 1 # up 170 | 171 | elif 90 <= x_angle <= 180: 172 | v = 2 # down 173 | 174 | if 180 <= y_angle <= 270: 175 | h = 3 # left 176 | 177 | elif 270 <= y_angle <= 360: 178 | h = 4 # right 179 | 180 | payload = {'report': 'tilted', 'value': [v, h]} 181 | self.publish_payload(payload, 'from_cpx_gateway') 182 | 183 | def switch_callback(self, data): 184 | """ 185 | This handles switches a, b, and slide 186 | :param data: data[1] - a=4 b=5, slice=7, 187 | """ 188 | if data[1] == 4: 189 | switch = 'a' 190 | elif data[1] == 5: 191 | switch = 'b' 192 | else: 193 | # 0 = right, 1 = left 194 | switch ='slide' 195 | 196 | payload = {'report': switch, 'value': data[2]} 197 | self.publish_payload(payload, 'from_cpx_gateway') 198 | 199 | def analog_callback(self, data): 200 | """ 201 | This handles the light, temperature and sound sensors. 202 | 203 | It also sets up a "watchdog timer" and if there is no activity 204 | for > 1 second will exit. 205 | 206 | 207 | :param data: data[1] - 8 = light, temp = 9, 10 = sound, 208 | """ 209 | 210 | self.last_analog_data_time = time.time() 211 | 212 | if data[1] == 8: 213 | sensor = 'light' 214 | elif data[1] == 9: 215 | sensor = 'temp' 216 | else: 217 | sensor = 'sound' 218 | payload = {'report': sensor, 'value': round(data[2], 2)} 219 | self.publish_payload(payload, 'from_cpx_gateway') 220 | 221 | def touchpad_callback(self, data): 222 | """ 223 | Build and send a banyan message for the pad and value 224 | :param data: data[1] = touchpad and data[2] = boolean value 225 | """ 226 | payload = {'report': 'touch' + str(data[1]), 'value': int(data[2])} 227 | self.publish_payload(payload, 'from_cpx_gateway') 228 | 229 | def shutdown(self): 230 | try: 231 | self.cpx.cpx_close_and_exit() 232 | sys.exit(0) 233 | except serial.serialutil.SerialException: 234 | pass 235 | 236 | def my_handler(self, xtype, value, tb): 237 | """ 238 | for logging uncaught exceptions 239 | :param xtype: 240 | :param value: 241 | :param tb: 242 | :return: 243 | """ 244 | self.logger.exception("Uncaught exception: {0}".format(str(value))) 245 | 246 | def run(self): 247 | if not self.last_analog_data_time: 248 | self.last_analog_data_time = time.time() 249 | while True: 250 | if time.time() - self.last_analog_data_time > 1.0: 251 | print('Watchdog timed out - exiting.') 252 | os._exit(1) 253 | 254 | time.sleep(1) 255 | 256 | 257 | def cpx_gateway(): 258 | parser = argparse.ArgumentParser() 259 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 260 | help="None or IP address used by Back Plane") 261 | parser.add_argument("-c", dest="com_port", default="None", 262 | help="Use this COM port instead of auto discovery") 263 | parser.add_argument("-l", dest="log", default="False", 264 | help="Set to True to turn logging on.") 265 | parser.add_argument("-m", dest="subscriber_list", 266 | default="to_cpx_gateway", nargs='+', 267 | help="Banyan topics space delimited: topic1 topic2 topic3") 268 | parser.add_argument("-n", dest="process_name", 269 | default="CpxGateway", help="Set process name in " 270 | "banner") 271 | parser.add_argument("-p", dest="publisher_port", default='43124', 272 | help="Publisher IP port") 273 | parser.add_argument("-r", dest="publisher_topic", 274 | default="from_cpx_gateway", help="Report topic") 275 | parser.add_argument("-s", dest="subscriber_port", default='43125', 276 | help="Subscriber IP port") 277 | 278 | args = parser.parse_args() 279 | 280 | subscriber_list = args.subscriber_list 281 | 282 | kw_options = { 283 | 'publisher_port': args.publisher_port, 284 | 'subscriber_port': args.subscriber_port, 285 | 'process_name': args.process_name, 286 | 'publisher_topic': args.publisher_topic 287 | } 288 | 289 | if args.back_plane_ip_address != 'None': 290 | kw_options['back_plane_ip_address'] = args.back_plane_ip_address 291 | 292 | if args.com_port != 'None': 293 | kw_options['com_port'] = args.com_port 294 | 295 | log = args.log.lower() 296 | if log == 'false': 297 | log = False 298 | else: 299 | log = True 300 | 301 | kw_options['log'] = log 302 | 303 | cpx = CpxGateway(subscriber_list, **kw_options) 304 | 305 | 306 | # signal handler function called when Control-C occurs 307 | # noinspection PyShadowingNames,PyUnusedLocal 308 | def signal_handler(sig, frame): 309 | print('Exiting Through Signal Handler') 310 | raise 311 | 312 | 313 | 314 | # listen for SIGINT 315 | signal.signal(signal.SIGINT, signal_handler) 316 | signal.signal(signal.SIGTERM, signal_handler) 317 | 318 | 319 | if __name__ == '__main__': 320 | cpx_gateway() 321 | -------------------------------------------------------------------------------- /s3_extend/gateways/esp32_gateway.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2024 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | 18 | import argparse 19 | import asyncio 20 | import signal 21 | import sys 22 | 23 | from python_banyan.gateway_base_aio import GatewayBaseAIO 24 | from telemetrix_aio_esp32 import telemetrix_aio_esp32 25 | 26 | 27 | # noinspection PyAbstractClass,PyMethodMayBeStatic,PyRedundantParentheses,DuplicatedCode,GrazieInspection 28 | class Esp32Gateway(GatewayBaseAIO): 29 | # This class implements the GatewayBase interface adapted for asyncio. 30 | # It supports ESP32 boards, tested with nodemcu. 31 | 32 | # NOTE: This class requires the use of Python 3.8 or above 33 | 34 | # serial_port = None 35 | 36 | def __init__(self, *subscriber_list, back_plane_ip_address=None, 37 | subscriber_port='43125', 38 | publisher_port='43124', process_name='Esp32Gateway', 39 | event_loop=None): 40 | """ 41 | Set up the gateway for operation 42 | 43 | :param subscriber_list: a tuple or list of subscription topics. 44 | :param back_plane_ip_address: ip address of backplane or none if local 45 | :param subscriber_port: backplane subscriber port 46 | :param publisher_port: backplane publisher port 47 | :param process_name: name to display on the console 48 | :param event_loop: optional parameter to pass in an asyncio 49 | event loop 50 | 51 | """ 52 | 53 | self.pin_info = {} 54 | self.gpio_pins = [2, 4, 5, 12, 13, 14, 16, 17, 18, 19, 21, 55 | 22, 23, 25, 26, 27, 32, 33, 34, 35, 36, 39] 56 | 57 | self.back_plane_ip_address = back_plane_ip_address 58 | self.publisher_port = publisher_port 59 | self.subscriber_port = subscriber_port 60 | 61 | self.subscriber_list = subscriber_list 62 | 63 | self.subscriber = None 64 | 65 | # set the event loop to be used. accept user's if provided 66 | self.event_loop = event_loop 67 | 68 | self.connection_socket = False 69 | 70 | self.esp = None 71 | 72 | self.transport_address = None 73 | 74 | # instantiate TelemetrixAIOEsp32 75 | # self.esp = telemetrix_aio_esp32.TelemetrixAioEsp32(autostart=False, 76 | # loop=self.event_loop) 77 | 78 | # Initialize the parent 79 | super(Esp32Gateway, self).__init__(subscriber_list=subscriber_list, 80 | event_loop=self.event_loop, 81 | back_plane_ip_address=back_plane_ip_address, 82 | subscriber_port=subscriber_port, 83 | publisher_port=publisher_port, 84 | process_name=process_name, 85 | ) 86 | 87 | # time.sleep(.5) 88 | 89 | def init_pins_dictionary(self): 90 | """ 91 | This method will initialize the pins dictionary contained 92 | in gateway base parent class. This method is called by 93 | the gateway base parent in its init method. 94 | 95 | NOTE: that this a a non-asyncio method. 96 | """ 97 | # pins data structure 98 | # pins supported: GPIO4, GPIO5, GPIO12, GPIO13, GPIO14, GPIO15 99 | # modes - IN - PULL_UP, OUT - OPEN_DRAIN, PWM 100 | # irqs - Pin.IRQ_RISING | Pin.IRQ_FALLING 101 | 102 | # build a status table for the pins 103 | for x in self.gpio_pins: 104 | entry = {'mode': None, 'pull_up': False, 'drain': False, 105 | 'irq': None, 'duty': None, 'freq': None, 'count': 0, 106 | 'value': 0} 107 | self.pin_info[x] = entry 108 | 109 | async def main(self): 110 | # call the inherited begin method located in banyan_base_aio 111 | await self.begin(start_loop=True) 112 | 113 | # wait for the first message that should provide the ip address of the WifI connection 114 | self.subscriber = await super(Esp32Gateway, self).get_subscriber() 115 | data = await self.subscriber.recv_multipart() 116 | payload = await self.unpack(data[1]) 117 | await self.additional_banyan_messages(None, payload) 118 | self.event_loop = asyncio.get_running_loop() 119 | # instantiate TelemetrixAIOEsp32 120 | self.esp = telemetrix_aio_esp32.TelemetrixAioEsp32(autostart=False, 121 | loop=self.event_loop, 122 | transport_is_wifi=True, 123 | transport_address=self.transport_address) 124 | await self.esp.start_aio() 125 | 126 | # sit in an endless loop to receive protocol messages 127 | # if start_loop: 128 | await self.begin_receive_loop() 129 | # while True: 130 | # # await self.receive_loop() 131 | # await asyncio.sleep(.0001) 132 | while True: 133 | await asyncio.sleep(.001) 134 | 135 | async def additional_banyan_messages(self, topic, payload): 136 | if payload['command'] == 'ip_address': 137 | # start up telemetrix-aio 138 | # if not self.connection_socket: 139 | # self.connection_socket = True 140 | # self.esp.ip_port = 31335 141 | # self.esp.ip_address = payload['address'] 142 | # await self.esp.start_aio() 143 | # await asyncio.sleep(1) 144 | self.transport_address = payload['address'] 145 | 146 | # The following methods and are called 147 | # by the gateway base class in its incoming_message_processing 148 | # method. They overwrite the default methods in the gateway_base. 149 | 150 | async def digital_write(self, topic, payload): 151 | """ 152 | This method performs a digital write 153 | :param topic: message topic 154 | :param payload: {"command": "digital_write", "pin": “PIN”, "value": “VALUE”} 155 | """ 156 | 157 | await self.esp.digital_write(payload["pin"], payload['value']) 158 | 159 | async def disable_analog_reporting(self, topic, payload): 160 | """ 161 | This method disables analog input reporting for the selected pin. 162 | :param topic: message topic 163 | :param payload: {"command": "disable_analog_reporting", "pin": “PIN”, "tag": "TAG"} 164 | """ 165 | await self.esp.disable_analog_reporting(payload["pin"]) 166 | 167 | async def disable_digital_reporting(self, topic, payload): 168 | """ 169 | This method disables digital input reporting for the selected pin. 170 | 171 | :param topic: message topic 172 | :param payload: {"command": "disable_digital_reporting", "pin": “PIN”, "tag": "TAG"} 173 | """ 174 | await self.esp.disable_digital_reporting(payload["pin"]) 175 | 176 | async def enable_analog_reporting(self, topic, payload): 177 | """ 178 | This method enables analog input reporting for the selected pin. 179 | :param topic: message topic 180 | :param payload: {"command": "enable_analog_reporting", "pin": “PIN”, "tag": "TAG"} 181 | """ 182 | await self.esp.enable_analog_reporting(payload["pin"]) 183 | 184 | async def enable_digital_reporting(self, topic, payload): 185 | """ 186 | This method enables digital input reporting for the selected pin. 187 | :param topic: message topic 188 | :param payload: {"command": "enable_digital_reporting", "pin": “PIN”, "tag": "TAG"} 189 | """ 190 | await self.esp.enable_digital_reporting(payload["pin"]) 191 | 192 | async def i2c_read(self, topic, payload): 193 | """ 194 | This method will perform an i2c read by specifying the i2c 195 | device address, i2c device register and the number of bytes 196 | to read. 197 | 198 | Call set_mode_i2c first to establish the pins for i2c operation. 199 | 200 | :param topic: message topic 201 | :param payload: {"command": "i2c_read", "pin": “PIN”, "tag": "TAG", 202 | "addr": “I2C ADDRESS, "register": “I2C REGISTER”, 203 | "number_of_bytes": “NUMBER OF BYTES”} 204 | :return via the i2c_callback method 205 | """ 206 | 207 | await self.esp.i2c_read(payload['addr'], 208 | payload['register'], 209 | payload['number_of_bytes'], callback=self.i2c_callback) 210 | 211 | async def i2c_write(self, topic, payload): 212 | """ 213 | This method will perform an i2c write for the i2c device with 214 | the specified i2c device address, i2c register and a list of byte 215 | to write. 216 | 217 | Call set_mode_i2c first to establish the pins for i2c operation. 218 | 219 | :param topic: message topic 220 | :param payload: {"command": "i2c_write", "pin": “PIN”, "tag": "TAG", 221 | "addr": “I2C ADDRESS, "register": “I2C REGISTER”, 222 | "data": [“DATA IN LIST FORM”]} 223 | """ 224 | await self.esp.i2c_write(payload['addr'], payload['data']) 225 | 226 | async def pwm_write(self, topic, payload): 227 | """ 228 | This method sets the pwm value for the selected pin. 229 | Call set_mode_pwm before calling this method. 230 | :param topic: message topic 231 | :param payload: {“command”: “pwm_write”, "pin": “PIN”, 232 | "tag":”TAG”, 233 | “value”: “VALUE”} 234 | """ 235 | await self.esp.analog_write(payload["pin"], payload['value']) 236 | 237 | async def servo_position(self, topic, payload): 238 | """ 239 | This method will set a servo's position in degrees. 240 | Call set_mode_servo first to activate the pin for 241 | servo operation. 242 | 243 | :param topic: message topic 244 | :param payload: {'command': 'servo_position', 245 | "pin": “PIN”,'tag': 'servo', 246 | “position”: “POSITION”} 247 | """ 248 | await self.esp.servo_write(payload["pin"], payload["position"]) 249 | 250 | async def set_mode_analog_input(self, topic, payload): 251 | """ 252 | This method sets a GPIO pin as analog input. 253 | :param topic: message topic 254 | :param payload: {"command": "set_mode_analog_input", "pin": “PIN”, "tag":”TAG” } 255 | """ 256 | pin = payload["pin"] 257 | await self.esp.set_pin_mode_analog_input(pin, callback=self.analog_input_callback) 258 | 259 | async def set_mode_digital_input(self, topic, payload): 260 | """ 261 | This method sets a pin as digital input. 262 | :param topic: message topic 263 | :param payload: {"command": "set_mode_digital_input", "pin": “PIN”, "tag":”TAG” } 264 | """ 265 | pin = payload["pin"] 266 | 267 | await self.esp.set_pin_mode_digital_input(pin, self.digital_input_callback) 268 | await asyncio.sleep(0.4) 269 | 270 | async def set_mode_digital_input_pullup(self, topic, payload): 271 | """ 272 | This method sets a pin as digital input with pull up enabled. 273 | :param topic: message topic 274 | :param payload: message payload 275 | """ 276 | pin = payload["pin"] 277 | 278 | await self.esp.set_pin_mode_digital_input_pullup(pin, self.digital_input_callback) 279 | 280 | async def set_mode_digital_output(self, topic, payload): 281 | """ 282 | This method sets a pin as a digital output pin. 283 | :param topic: message topic 284 | :param payload: {"command": "set_mode_digital_output", "pin": PIN, "tag":”TAG” } 285 | """ 286 | pin = payload["pin"] 287 | 288 | await self.esp.set_pin_mode_digital_output(pin) 289 | 290 | async def set_mode_i2c(self, topic, payload): 291 | """ 292 | This method sets up the i2c pins for i2c operations. 293 | :param topic: message topic 294 | :param payload: {"command": "set_mode_i2c"} 295 | """ 296 | # self.pins_dictionary[200][GatewayBaseAIO.PIN_MODE] = GatewayBaseAIO.I2C_MODE 297 | await self.esp.set_pin_mode_i2c() 298 | 299 | async def set_mode_pwm(self, topic, payload): 300 | """ 301 | This method sets a GPIO pin capable of PWM for PWM operation. 302 | :param topic: message topic 303 | :param payload: {"command": "set_mode_pwm", "pin": “PIN”, "tag":”TAG” } 304 | """ 305 | pin = payload["pin"] 306 | 307 | await self.esp.set_pin_mode_analog_output(pin) 308 | 309 | async def set_mode_servo(self, topic, payload): 310 | """ 311 | This method establishes a GPIO pin for servo operation. 312 | :param topic: message topic 313 | :param payload: {"command": "set_mode_servo", "pin": “PIN”, "tag":”TAG” } 314 | """ 315 | pin = payload["pin"] 316 | 317 | await self.esp.set_pin_mode_servo(pin) 318 | 319 | async def set_mode_sonar(self, topic, payload): 320 | """ 321 | This method sets the trigger and echo pins for sonar operation. 322 | :param topic: message topic 323 | :param payload: {"command": "set_mode_sonar", "trigger_pin": “PIN”, "tag":”TAG” 324 | "echo_pin": “PIN”"tag":”TAG” } 325 | """ 326 | 327 | trigger = payload["trigger_pin"] 328 | echo = payload["echo_pin"] 329 | 330 | await self.esp.set_pin_mode_sonar(trigger, echo, self.sonar_callback) 331 | 332 | # Callbacks 333 | async def digital_input_callback(self, data): 334 | """ 335 | Digital input data change reported by ESP 336 | :param data: 337 | :return: 338 | """ 339 | payload = {'report': 'digital_input', 'pin': data[1], 340 | 'value': data[2], 'timestamp': data[3]} 341 | await self.publish_payload(payload, 'from_esp32_gateway') 342 | 343 | async def analog_input_callback(self, data): 344 | payload = {'report': 'analog_input', 'pin': data[1], 345 | 'value': data[2], 'timestamp': data[3]} 346 | await self.publish_payload(payload, 'from_esp32_gateway') 347 | 348 | async def i2c_callback(self, data): 349 | """ 350 | Analog input data change reported by ESP 351 | 352 | :param data: 353 | :return: 354 | """ 355 | # creat a string representation of the data returned 356 | report = ', '.join([str(elem) for elem in data]) 357 | payload = {'report': 'i2c_data', 'value': report} 358 | await self.publish_payload(payload, 'from_esp32_gateway') 359 | 360 | async def sonar_callback(self, data): 361 | """ 362 | Sonar data change reported by ESP 363 | 364 | :param data: 365 | :return: 366 | """ 367 | payload = {'report': 'sonar_data', 'value': data[2]} 368 | await self.publish_payload(payload, 'from_esp32_gateway') 369 | 370 | 371 | # noinspection DuplicatedCode 372 | def esp32_gateway(): 373 | # allow user to bypass the IP address auto-discovery. This is necessary if the component resides on a computer 374 | # other than the computing running the backplane. 375 | 376 | parser = argparse.ArgumentParser() 377 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 378 | help="None or IP address used by Back Plane") 379 | parser.add_argument("-c", dest="com_port", default="None", 380 | help="Use this COM port instead of auto discovery") 381 | parser.add_argument("-m", dest="subscriber_list", 382 | default="to_esp32_gateway", nargs='+', 383 | help="Banyan topics space delimited: topic1 topic2 topic3") 384 | parser.add_argument("-n", dest="process_name", 385 | default="esp32Gateway", help="Set process name in " 386 | "banner") 387 | parser.add_argument("-p", dest="publisher_port", default='43124', 388 | help="Publisher IP port") 389 | parser.add_argument("-r", dest="publisher_topic", 390 | default="from_rpi_gpio", help="Report topic") 391 | parser.add_argument("-s", dest="subscriber_port", default='43125', 392 | help="Subscriber IP port") 393 | 394 | args = parser.parse_args() 395 | 396 | subscriber_list = args.subscriber_list 397 | 398 | kw_options = { 399 | 'publisher_port': args.publisher_port, 400 | 'subscriber_port': args.subscriber_port, 401 | 'process_name': args.process_name, 402 | } 403 | if args.back_plane_ip_address != 'None': 404 | kw_options['back_plane_ip_address'] = args.back_plane_ip_address 405 | 406 | if args.com_port != 'None': 407 | kw_options['com_port'] = args.com_port 408 | 409 | # get the event loop 410 | # this is for python 3.8 411 | if sys.platform == 'win32': 412 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 413 | 414 | loop = asyncio.get_event_loop() 415 | 416 | # replace with the name of your class 417 | app = Esp32Gateway(subscriber_list, **kw_options, event_loop=loop) 418 | 419 | try: 420 | loop.run_until_complete(app.main()) 421 | except (KeyboardInterrupt, asyncio.CancelledError, RuntimeError): 422 | loop.stop() 423 | loop.close() 424 | sys.exit(0) 425 | 426 | 427 | # signal handler function called when Control-C occurs 428 | # noinspection PyShadowingNames,PyUnusedLocal 429 | def signal_handler(sig, frame): 430 | print('Exiting Through Signal Handler') 431 | raise KeyboardInterrupt 432 | 433 | 434 | # listen for SIGINT 435 | signal.signal(signal.SIGINT, signal_handler) 436 | signal.signal(signal.SIGTERM, signal_handler) 437 | 438 | if __name__ == '__main__': 439 | esp32_gateway() 440 | -------------------------------------------------------------------------------- /s3_extend/gateways/esp8266_gateway.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2018-2021 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | 18 | import argparse 19 | import asyncio 20 | import signal 21 | import sys 22 | 23 | from python_banyan.gateway_base_aio import GatewayBaseAIO 24 | from telemetrix_aio.telemetrix_aio import TelemetrixAIO 25 | 26 | 27 | # noinspection PyAbstractClass,PyMethodMayBeStatic,PyRedundantParentheses,DuplicatedCode,GrazieInspection 28 | class Esp8266Gateway(GatewayBaseAIO): 29 | # This class implements the GatewayBase interface adapted for asyncio. 30 | # It supports ESP8266 boards, tested with nodemcu. 31 | 32 | # NOTE: This class requires the use of Python 3.8 or above 33 | 34 | # serial_port = None 35 | 36 | def __init__(self, *subscriber_list, back_plane_ip_address=None, 37 | subscriber_port='43125', 38 | publisher_port='43124', process_name='Esp8266Gateway', 39 | event_loop=None): 40 | """ 41 | Set up the gateway for operation 42 | 43 | :param subscriber_list: a tuple or list of subscription topics. 44 | :param back_plane_ip_address: ip address of backplane or none if local 45 | :param subscriber_port: backplane subscriber port 46 | :param publisher_port: backplane publisher port 47 | :param process_name: name to display on the console 48 | :param event_loop: optional parameter to pass in an asyncio 49 | event loop 50 | 51 | """ 52 | 53 | self.pin_info = {} 54 | self.gpio_pins = [4, 5, 12, 13, 14, 15] 55 | 56 | self.back_plane_ip_address = back_plane_ip_address 57 | self.publisher_port = publisher_port 58 | self.subscriber_port = subscriber_port 59 | 60 | self.subscriber_list = subscriber_list 61 | 62 | self.subscriber = None 63 | 64 | # set the event loop to be used. accept user's if provided 65 | self.event_loop = event_loop 66 | 67 | self.connection_socket = False 68 | 69 | # instantiate TelemetrixAIO 70 | # if user want to pass in a com port, then pass it in 71 | self.esp = TelemetrixAIO(autostart=False, loop=self.event_loop, com_port=None) 72 | 73 | # the_subscriber.setsockopt(zmq.SUBSCRIBE, "abc".encode()) 74 | # data = await self.subscriber.recv_multipart() 75 | # Initialize the parent 76 | super(Esp8266Gateway, self).__init__(subscriber_list=subscriber_list, 77 | event_loop=self.event_loop, 78 | back_plane_ip_address=back_plane_ip_address, 79 | subscriber_port=subscriber_port, 80 | publisher_port=publisher_port, 81 | process_name=process_name, 82 | ) 83 | 84 | # time.sleep(.5) 85 | 86 | def init_pins_dictionary(self): 87 | """ 88 | This method will initialize the pins dictionary contained 89 | in gateway base parent class. This method is called by 90 | the gateway base parent in its init method. 91 | 92 | NOTE: that this a a non-asyncio method. 93 | """ 94 | # pins data structure 95 | # pins supported: GPIO4, GPIO5, GPIO12, GPIO13, GPIO14, GPIO15 96 | # modes - IN - PULL_UP, OUT - OPEN_DRAIN, PWM 97 | # irqs - Pin.IRQ_RISING | Pin.IRQ_FALLING 98 | 99 | # build a status table for the pins 100 | for x in self.gpio_pins: 101 | entry = {'mode': None, 'pull_up': False, 'drain': False, 102 | 'irq': None, 'duty': None, 'freq': None, 'count': 0, 103 | 'value': 0} 104 | self.pin_info[x] = entry 105 | 106 | async def main(self): 107 | # call the inherited begin method located in banyan_base_aio 108 | await self.begin(start_loop=False) 109 | 110 | # wait for the first message that should provide the ip address of the WifI connection 111 | self.subscriber = await super(Esp8266Gateway, self).get_subscriber() 112 | data = await self.subscriber.recv_multipart() 113 | payload = await self.unpack(data[1]) 114 | await self.additional_banyan_messages(None, payload) 115 | # sit in an endless loop to receive protocol messages 116 | # if start_loop: 117 | await self.begin_receive_loop() 118 | # while True: 119 | # # await self.receive_loop() 120 | # await asyncio.sleep(.0001) 121 | while True: 122 | await asyncio.sleep(.001) 123 | 124 | async def additional_banyan_messages(self, topic, payload): 125 | if payload['command'] == 'ip_address': 126 | # start up telemetrix-aio 127 | if not self.connection_socket: 128 | self.connection_socket = True 129 | self.esp.ip_port = 31335 130 | self.esp.ip_address = payload['address'] 131 | await self.esp.start_aio() 132 | # await asyncio.sleep(1) 133 | 134 | # The following methods and are called 135 | # by the gateway base class in its incoming_message_processing 136 | # method. They overwrite the default methods in the gateway_base. 137 | 138 | async def digital_write(self, topic, payload): 139 | """ 140 | This method performs a digital write 141 | :param topic: message topic 142 | :param payload: {"command": "digital_write", "pin": “PIN”, "value": “VALUE”} 143 | """ 144 | 145 | await self.esp.digital_write(payload["pin"], payload['value']) 146 | 147 | async def disable_analog_reporting(self, topic, payload): 148 | """ 149 | This method disables analog input reporting for the selected pin. 150 | :param topic: message topic 151 | :param payload: {"command": "disable_analog_reporting", "pin": “PIN”, "tag": "TAG"} 152 | """ 153 | await self.esp.disable_analog_reporting(payload["pin"]) 154 | 155 | async def disable_digital_reporting(self, topic, payload): 156 | """ 157 | This method disables digital input reporting for the selected pin. 158 | 159 | :param topic: message topic 160 | :param payload: {"command": "disable_digital_reporting", "pin": “PIN”, "tag": "TAG"} 161 | """ 162 | await self.esp.disable_digital_reporting(payload["pin"]) 163 | 164 | async def enable_analog_reporting(self, topic, payload): 165 | """ 166 | This method enables analog input reporting for the selected pin. 167 | :param topic: message topic 168 | :param payload: {"command": "enable_analog_reporting", "pin": “PIN”, "tag": "TAG"} 169 | """ 170 | await self.esp.enable_analog_reporting(payload["pin"]) 171 | 172 | async def enable_digital_reporting(self, topic, payload): 173 | """ 174 | This method enables digital input reporting for the selected pin. 175 | :param topic: message topic 176 | :param payload: {"command": "enable_digital_reporting", "pin": “PIN”, "tag": "TAG"} 177 | """ 178 | await self.esp.enable_digital_reporting(payload["pin"]) 179 | 180 | async def i2c_read(self, topic, payload): 181 | """ 182 | This method will perform an i2c read by specifying the i2c 183 | device address, i2c device register and the number of bytes 184 | to read. 185 | 186 | Call set_mode_i2c first to establish the pins for i2c operation. 187 | 188 | :param topic: message topic 189 | :param payload: {"command": "i2c_read", "pin": “PIN”, "tag": "TAG", 190 | "addr": “I2C ADDRESS, "register": “I2C REGISTER”, 191 | "number_of_bytes": “NUMBER OF BYTES”} 192 | :return via the i2c_callback method 193 | """ 194 | 195 | await self.esp.i2c_read(payload['addr'], 196 | payload['register'], 197 | payload['number_of_bytes'], callback=self.i2c_callback) 198 | 199 | async def i2c_write(self, topic, payload): 200 | """ 201 | This method will perform an i2c write for the i2c device with 202 | the specified i2c device address, i2c register and a list of byte 203 | to write. 204 | 205 | Call set_mode_i2c first to establish the pins for i2c operation. 206 | 207 | :param topic: message topic 208 | :param payload: {"command": "i2c_write", "pin": “PIN”, "tag": "TAG", 209 | "addr": “I2C ADDRESS, "register": “I2C REGISTER”, 210 | "data": [“DATA IN LIST FORM”]} 211 | """ 212 | await self.esp.i2c_write(payload['addr'], payload['data']) 213 | 214 | async def pwm_write(self, topic, payload): 215 | """ 216 | This method sets the pwm value for the selected pin. 217 | Call set_mode_pwm before calling this method. 218 | :param topic: message topic 219 | :param payload: {“command”: “pwm_write”, "pin": “PIN”, 220 | "tag":”TAG”, 221 | “value”: “VALUE”} 222 | """ 223 | await self.esp.analog_write(payload["pin"], payload['value']) 224 | 225 | async def servo_position(self, topic, payload): 226 | """ 227 | This method will set a servo's position in degrees. 228 | Call set_mode_servo first to activate the pin for 229 | servo operation. 230 | 231 | :param topic: message topic 232 | :param payload: {'command': 'servo_position', 233 | "pin": “PIN”,'tag': 'servo', 234 | “position”: “POSITION”} 235 | """ 236 | await self.esp.servo_write(payload["pin"], payload["position"]) 237 | 238 | async def set_mode_analog_input(self, topic, payload): 239 | """ 240 | This method sets a GPIO pin as analog input. 241 | :param topic: message topic 242 | :param payload: {"command": "set_mode_analog_input", "pin": “PIN”, "tag":”TAG” } 243 | """ 244 | pin = payload["pin"] 245 | await self.esp.set_pin_mode_analog_input(pin, callback=self.analog_input_callback) 246 | 247 | async def set_mode_digital_input(self, topic, payload): 248 | """ 249 | This method sets a pin as digital input. 250 | :param topic: message topic 251 | :param payload: {"command": "set_mode_digital_input", "pin": “PIN”, "tag":”TAG” } 252 | """ 253 | pin = payload["pin"] 254 | 255 | await self.esp.set_pin_mode_digital_input(pin, self.digital_input_callback) 256 | # await asyncio.sleep(0.4) 257 | 258 | async def set_mode_digital_input_pullup(self, topic, payload): 259 | """ 260 | This method sets a pin as digital input with pull up enabled. 261 | :param topic: message topic 262 | :param payload: message payload 263 | """ 264 | pin = payload["pin"] 265 | 266 | await self.esp.set_pin_mode_digital_input_pullup(pin, self.digital_input_callback) 267 | 268 | async def set_mode_digital_output(self, topic, payload): 269 | """ 270 | This method sets a pin as a digital output pin. 271 | :param topic: message topic 272 | :param payload: {"command": "set_mode_digital_output", "pin": PIN, "tag":”TAG” } 273 | """ 274 | pin = payload["pin"] 275 | 276 | await self.esp.set_pin_mode_digital_output(pin) 277 | 278 | async def set_mode_i2c(self, topic, payload): 279 | """ 280 | This method sets up the i2c pins for i2c operations. 281 | :param topic: message topic 282 | :param payload: {"command": "set_mode_i2c"} 283 | """ 284 | # self.pins_dictionary[200][GatewayBaseAIO.PIN_MODE] = GatewayBaseAIO.I2C_MODE 285 | await self.esp.set_pin_mode_i2c() 286 | 287 | async def set_mode_pwm(self, topic, payload): 288 | """ 289 | This method sets a GPIO pin capable of PWM for PWM operation. 290 | :param topic: message topic 291 | :param payload: {"command": "set_mode_pwm", "pin": “PIN”, "tag":”TAG” } 292 | """ 293 | pin = payload["pin"] 294 | 295 | await self.esp.set_pin_mode_analog_output(pin) 296 | 297 | async def set_mode_servo(self, topic, payload): 298 | """ 299 | This method establishes a GPIO pin for servo operation. 300 | :param topic: message topic 301 | :param payload: {"command": "set_mode_servo", "pin": “PIN”, "tag":”TAG” } 302 | """ 303 | pin = payload["pin"] 304 | 305 | await self.esp.set_pin_mode_servo(pin) 306 | 307 | async def set_mode_sonar(self, topic, payload): 308 | """ 309 | This method sets the trigger and echo pins for sonar operation. 310 | :param topic: message topic 311 | :param payload: {"command": "set_mode_sonar", "trigger_pin": “PIN”, "tag":”TAG” 312 | "echo_pin": “PIN”"tag":”TAG” } 313 | """ 314 | 315 | trigger = payload["trigger_pin"] 316 | echo = payload["echo_pin"] 317 | 318 | await self.esp.set_pin_mode_sonar(trigger, echo, self.sonar_callback) 319 | 320 | # Callbacks 321 | async def digital_input_callback(self, data): 322 | """ 323 | Digital input data change reported by ESP 324 | :param data: 325 | :return: 326 | """ 327 | payload = {'report': 'digital_input', 'pin': data[1], 328 | 'value': data[2], 'timestamp': data[3]} 329 | await self.publish_payload(payload, 'from_esp8266_gateway') 330 | 331 | async def analog_input_callback(self, data): 332 | payload = {'report': 'analog_input', 'pin': data[1], 333 | 'value': data[2], 'timestamp': data[3]} 334 | await self.publish_payload(payload, 'from_esp8266_gateway') 335 | 336 | async def i2c_callback(self, data): 337 | """ 338 | Analog input data change reported by ESP 339 | 340 | :param data: 341 | :return: 342 | """ 343 | # creat a string representation of the data returned 344 | report = ', '.join([str(elem) for elem in data]) 345 | payload = {'report': 'i2c_data', 'value': report} 346 | await self.publish_payload(payload, 'from_esp8266_gateway') 347 | 348 | async def sonar_callback(self, data): 349 | """ 350 | Sonar data change reported by ESP 351 | 352 | :param data: 353 | :return: 354 | """ 355 | payload = {'report': 'sonar_data', 'value': data[2]} 356 | await self.publish_payload(payload, 'from_esp8266_gateway') 357 | 358 | 359 | # noinspection DuplicatedCode 360 | def esp8266_gateway(): 361 | # allow user to bypass the IP address auto-discovery. This is necessary if the component resides on a computer 362 | # other than the computing running the backplane. 363 | 364 | parser = argparse.ArgumentParser() 365 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 366 | help="None or IP address used by Back Plane") 367 | parser.add_argument("-c", dest="com_port", default="None", 368 | help="Use this COM port instead of auto discovery") 369 | parser.add_argument("-m", dest="subscriber_list", 370 | default="to_esp8266_gateway", nargs='+', 371 | help="Banyan topics space delimited: topic1 topic2 topic3") 372 | parser.add_argument("-n", dest="process_name", 373 | default="ESP8266Gateway", help="Set process name in " 374 | "banner") 375 | parser.add_argument("-p", dest="publisher_port", default='43124', 376 | help="Publisher IP port") 377 | parser.add_argument("-r", dest="publisher_topic", 378 | default="from_rpi_gpio", help="Report topic") 379 | parser.add_argument("-s", dest="subscriber_port", default='43125', 380 | help="Subscriber IP port") 381 | 382 | args = parser.parse_args() 383 | 384 | subscriber_list = args.subscriber_list 385 | 386 | kw_options = { 387 | 'publisher_port': args.publisher_port, 388 | 'subscriber_port': args.subscriber_port, 389 | 'process_name': args.process_name, 390 | } 391 | if args.back_plane_ip_address != 'None': 392 | kw_options['back_plane_ip_address'] = args.back_plane_ip_address 393 | 394 | if args.com_port != 'None': 395 | kw_options['com_port'] = args.com_port 396 | 397 | # get the event loop 398 | # this is for python 3.8 399 | if sys.platform == 'win32': 400 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 401 | 402 | loop = asyncio.get_event_loop() 403 | 404 | # replace with the name of your class 405 | app = Esp8266Gateway(subscriber_list, **kw_options, event_loop=loop) 406 | 407 | try: 408 | loop.run_until_complete(app.main()) 409 | except (KeyboardInterrupt, asyncio.CancelledError, RuntimeError): 410 | loop.stop() 411 | loop.close() 412 | sys.exit(0) 413 | 414 | 415 | # signal handler function called when Control-C occurs 416 | # noinspection PyShadowingNames,PyUnusedLocal 417 | def signal_handler(sig, frame): 418 | print('Exiting Through Signal Handler') 419 | raise KeyboardInterrupt 420 | 421 | 422 | # listen for SIGINT 423 | signal.signal(signal.SIGINT, signal_handler) 424 | signal.signal(signal.SIGTERM, signal_handler) 425 | 426 | if __name__ == '__main__': 427 | esp8266_gateway() 428 | -------------------------------------------------------------------------------- /s3_extend/gateways/picoboard_gateway.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This is the Python Banyan GUI that communicates with 5 | the Raspberry Pi Banyan Gateway 6 | 7 | Copyright (c) 2019 Alan Yorinks All right reserved. 8 | 9 | Python Banyan is free software; you can redistribute it and/or 10 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 11 | Version 3 as published by the Free Software Foundation; either 12 | or (at your option) any later version. 13 | This library is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | General Public License for more details. 17 | 18 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 19 | along with this library; if not, write to the Free Software 20 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 | 22 | """ 23 | import argparse 24 | import atexit 25 | # noinspection PyPackageRequirements 26 | import logging 27 | import pathlib 28 | import serial 29 | # noinspection PyPackageRequirements 30 | from serial.tools import list_ports 31 | import signal 32 | import sys 33 | # import threading 34 | import time 35 | from python_banyan.banyan_base import BanyanBase 36 | 37 | 38 | # noinspection PyMethodMayBeStatic 39 | class PicoboardGateway(BanyanBase): 40 | """ 41 | This class is the interface class for the picoboard supporting 42 | Scratch 3. 43 | 44 | It will regularly poll the picoboard, normalize the sensor data, and then 45 | publish it. Data is published in the form of a list. 46 | """ 47 | 48 | def __init__(self, back_plane_ip_address=None, subscriber_port='43125', 49 | publisher_port='43124', process_name='PicoboardGateway', 50 | com_port=None, publisher_topic=None, log=False): 51 | """ 52 | :param back_plane_ip_address: 53 | :param subscriber_port: 54 | :param publisher_port: 55 | :param process_name: 56 | :param com_port: picoboard com_port 57 | """ 58 | 59 | # initialize parent 60 | super(PicoboardGateway, self).__init__(back_plane_ip_address, subscriber_port, 61 | publisher_port, process_name=process_name) 62 | self.log = log 63 | if self.log: 64 | fn = str(pathlib.Path.home()) + "/pbgw.log" 65 | self.logger = logging.getLogger(__name__) 66 | logging.basicConfig(filename=fn, filemode='w', level=logging.DEBUG) 67 | sys.excepthook = self.my_handler 68 | 69 | self.baud_rate = 38400 70 | self.publisher_topic = publisher_topic 71 | 72 | atexit.register(self.shutdown) 73 | 74 | # place to receive data from picoboard 75 | self.data_packet = None 76 | 77 | # data value positions in the data stream 78 | # generated by the picoboard 79 | 80 | # position 0 = board id 81 | # position 1 = D analog inverted logic 82 | # position 2 = C analog inverted logic 83 | # position 3 = B analog inverted logic 84 | # position 4 = Button digital inverted logic 85 | # position 5 = A analog inverted logic 86 | # position 6 = Light analog inverted logic 87 | # position 7 = sound analog 88 | # position 8 = slider analog 89 | 90 | # positional indices into data stream for analog sensors 91 | # light is a special case and removed from this list 92 | self.analog_sensor_list = [1, 2, 3, 5, 7, 8] 93 | 94 | # positional values for specific sensor types 95 | self.button_position = 4 96 | self.light_position = 6 97 | self.sound_position = 7 98 | 99 | # indices that require data inversion. 100 | # light is left out of this list and handled separately. 101 | self.inverted_analog_list = [1, 2, 3, 5] 102 | 103 | # The payload data is built as a list of entries. 104 | # Indices are as follows: 105 | # index 0 = D analog inverted logic 106 | # index 1 = C analog inverted logic 107 | # index 2 = B analog inverted logic 108 | # index 3 = Button digital inverted logic 109 | # index 4 = A analog inverted logic 110 | # index 5 = Light analog inverted logic 111 | # index 6 = sound analog 112 | # index 7 = slider analog 113 | 114 | self.payload = {'report': []} 115 | 116 | # poll request for picoboard data 117 | self.poll_byte = b'\x01' 118 | 119 | # if a com port was specified use it. 120 | if com_port: 121 | self.picoboard = serial.Serial(com_port, self.baud_rate, 122 | timeout=1, writeTimeout=0) 123 | # otherwise try to find a picoboard 124 | else: 125 | if self.find_the_picoboard(): 126 | print('picoboard found on:', self.picoboard.port) 127 | else: 128 | # if not self.find_the_picoboard(): 129 | print('Cannot find Picoboard') 130 | sys.exit() 131 | 132 | # allow thread time to start 133 | time.sleep(.2) 134 | self.picoboard.write(self.poll_byte) 135 | 136 | while True: 137 | self.data_packet = None 138 | # if there is data available from the picoboard 139 | # retrieve 18 bytes - a full picoboard packet 140 | cooked = None 141 | try: 142 | while self.picoboard.in_waiting != 18: 143 | # no data available, just kill some time 144 | try: 145 | time.sleep(.001) 146 | except (KeyboardInterrupt, serial.SerialException): 147 | sys.exit() 148 | except (KeyboardInterrupt, serial.SerialException): 149 | sys.exit() 150 | self.data_packet = self.picoboard.read(18) 151 | # get the channel number and data for the channel 152 | for i in range(9): 153 | # first channel reporting 154 | if i == 0: 155 | pico_channel = (int(self.data_packet[0]) - 128) >> 3 156 | if pico_channel != 15 and pico_channel != 0: 157 | continue 158 | # check if the channel data is a value of 4 159 | pico_data = int(self.data_packet[1]) 160 | if pico_data != 4: 161 | break 162 | # pico_channel = self.channels[(int(self.data_packet[2 * i]) - 128) >> 3] 163 | raw_sensor_value = ((int(self.data_packet[2 * i]) & 7) << 7) + int(self.data_packet[2 * i + 1]) 164 | if i == 0: # id 165 | cooked = raw_sensor_value 166 | elif i == self.light_position: 167 | if raw_sensor_value < 25: 168 | cooked = 100 - raw_sensor_value 169 | else: 170 | cooked = round((1023 - raw_sensor_value) * (75 / 998)) 171 | cooked = self.analog_scaling(cooked, self.light_position) 172 | 173 | elif i == self.sound_position: 174 | n = max(0, raw_sensor_value - 18) 175 | if n < 50: 176 | cooked = int(n / 2) 177 | else: 178 | cooked = 25 + min(75, int((n - 50) * (75 / 580))) 179 | elif i == self.button_position: # invert digital input 180 | cooked = int(not raw_sensor_value) 181 | 182 | if i in self.analog_sensor_list: 183 | # scale for standard analog: 184 | cooked = self.analog_scaling(raw_sensor_value, i) 185 | 186 | # don't add the firmware id to the payload - 187 | # the extension does not need it. 188 | if i != 0: 189 | self.payload['report'].append(cooked) 190 | 191 | self.publish_payload(self.payload, self.publisher_topic) 192 | self.payload = {'report': []} 193 | self.picoboard.write(self.poll_byte) 194 | 195 | def find_the_picoboard(self): 196 | """ 197 | Go through the ports looking for an active board 198 | """ 199 | 200 | try: 201 | the_ports_list = list_ports.comports() 202 | except (KeyboardInterrupt, serial.SerialException): 203 | sys.exit(0) 204 | 205 | for port in the_ports_list: 206 | if port.pid is None: 207 | continue 208 | else: 209 | print('Looking for picoboard on: ', port.device) 210 | try: 211 | self.picoboard = serial.Serial(port.device, self.baud_rate, 212 | timeout=1, writeTimeout=0) 213 | except (KeyboardInterrupt, serial.SerialException): 214 | sys.exit(0) 215 | try: 216 | self.picoboard.write(self.poll_byte) 217 | time.sleep(.2) 218 | except (KeyboardInterrupt, serial.SerialException): 219 | sys.exit() 220 | 221 | try_count = 10 222 | while try_count: 223 | num_bytes = self.picoboard.in_waiting 224 | if num_bytes < 18: 225 | try: 226 | self.picoboard.write(self.poll_byte) 227 | time.sleep(.2) 228 | try_count -= 1 229 | if not try_count: 230 | # return False 231 | continue 232 | except (KeyboardInterrupt, serial.SerialException): 233 | sys.exit() 234 | # check the first 2 bytes for channel 0 or f 235 | else: 236 | data_packet = self.picoboard.read(18) 237 | pico_channel = (int(data_packet[0]) - 128) >> 3 238 | if pico_channel != 15 and pico_channel != 0: 239 | continue 240 | # check if the channel data is a value of 4 241 | pico_data = int(data_packet[1]) 242 | 243 | if pico_data != 4: 244 | return False 245 | else: 246 | return True 247 | 248 | def analog_scaling(self, value, index): 249 | """ 250 | scale the normal analog input range of 0-1023 to 0-100 251 | :param value: 252 | :param index: sensor index value within data stream 253 | :return: A value scaled between 0 and 100 254 | """ 255 | if index == self.light_position: 256 | input_low = 0 257 | input_high = 100 258 | elif index in self.inverted_analog_list: # the light channel 259 | input_low = 1023 260 | input_high = 0 261 | else: 262 | input_low = 0 263 | input_high = 1023 264 | 265 | new_value_low = 0 266 | new_value_high = 100 267 | 268 | return round(((value - input_low) * ((new_value_high - new_value_low) / (input_high - input_low))) + 269 | new_value_low) 270 | 271 | def my_handler(self, xtype, value, tb): 272 | """ 273 | for logging uncaught exceptions 274 | :param xtype: 275 | :param value: 276 | :param tb: 277 | :return: 278 | """ 279 | self.logger.exception("Uncaught exception: {0}".format(str(value))) 280 | 281 | def shutdown(self): 282 | """ 283 | Exit gracefully 284 | 285 | """ 286 | self.picoboard.reset_input_buffer() 287 | self.picoboard.reset_output_buffer() 288 | self.picoboard.close() 289 | sys.exit(0) 290 | 291 | 292 | def picoboard_gateway(): 293 | parser = argparse.ArgumentParser() 294 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 295 | help="None or IP address used by Back Plane") 296 | parser.add_argument("-c", dest="com_port", default="None", 297 | help="Use this COM port instead of auto discovery") 298 | parser.add_argument("-l", dest="log", default="False", 299 | help="Set to True to turn logging on.") 300 | parser.add_argument("-n", dest="process_name", 301 | default="PicoboardGateway", help="Set process name in " 302 | "banner") 303 | parser.add_argument("-p", dest="publisher_port", default='43124', 304 | help="Publisher IP port") 305 | parser.add_argument("-r", dest="publisher_topic", 306 | default="from_picoboard_gateway", help="Report topic") 307 | parser.add_argument("-s", dest="subscriber_port", default='43125', 308 | help="Subscriber IP port") 309 | 310 | args = parser.parse_args() 311 | 312 | kw_options = { 313 | 'publisher_port': args.publisher_port, 314 | 'subscriber_port': args.subscriber_port, 315 | 'process_name': args.process_name, 316 | 'publisher_topic': args.publisher_topic 317 | } 318 | 319 | if args.back_plane_ip_address != 'None': 320 | kw_options['back_plane_ip_address'] = args.back_plane_ip_address 321 | 322 | if args.com_port != 'None': 323 | kw_options['com_port'] = args.com_port 324 | 325 | log = args.log.lower() 326 | if log == 'false': 327 | log = False 328 | else: 329 | log = True 330 | 331 | kw_options['log'] = log 332 | 333 | PicoboardGateway(**kw_options) 334 | 335 | 336 | # signal handler function called when Control-C occurs 337 | # noinspection PyShadowingNames,PyUnusedLocal 338 | def signal_handler(sig, frame): 339 | print('Exiting Through Signal Handler') 340 | raise KeyboardInterrupt 341 | 342 | 343 | # listen for SIGINT 344 | signal.signal(signal.SIGINT, signal_handler) 345 | signal.signal(signal.SIGTERM, signal_handler) 346 | 347 | if __name__ == '__main__': 348 | picoboard_gateway() 349 | -------------------------------------------------------------------------------- /s3_extend/gateways/robohat_gateway.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | robohat_gateway.py 5 | 6 | This is the OneGPIO gateway for pymata_rh 7 | 8 | Copyright (c) 2020 Alan Yorinks All right reserved. 9 | 10 | This software is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 12 | Version 3 as published by the Free Software Foundation; either 13 | or (at your option) any later version. 14 | This library is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 20 | along with this library; if not, write to the Free Software 21 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 22 | 23 | """ 24 | 25 | from python_banyan.gateway_base import GatewayBase 26 | from pymata_rh import pymata_rh 27 | import argparse 28 | import signal 29 | import sys 30 | 31 | 32 | class RoboHatGateway(GatewayBase): 33 | """ 34 | This is a good starting point for creating your own Banyan GPIO Gateway 35 | 36 | Search for GpioGatewayTemplate and gpio_gateway_template, and then replace 37 | with a names of your own making. 38 | 39 | Change the -l and -n options at the bottom of this file 40 | to be consistent with the specific board type 41 | """ 42 | 43 | # noinspection PyDefaultArgument,PyRedundantParentheses 44 | def __init__(self, *subscriber_list, **kwargs): 45 | """ 46 | 47 | :param subscriber_list: a tuple or list of topics to be subscribed to 48 | :param kwargs: contains the following parameters: 49 | 50 | back_plane_ip_address: banyan_base back_planeIP Address - 51 | if not specified, it will be set to the local computer 52 | subscriber_port: banyan_base back plane subscriber port. 53 | This must match that of the banyan_base backplane 54 | publisher_port: banyan_base back plane publisher port. 55 | This must match that of the banyan_base 56 | backplane 57 | process_name: Component identifier 58 | board_type: target micro-controller type ID 59 | 60 | """ 61 | # initialize the parent 62 | super(RoboHatGateway, self).__init__( 63 | subscriber_list=subscriber_list, 64 | back_plane_ip_address=kwargs[ 65 | 'back_plane_ip_address'], 66 | subscriber_port=kwargs[ 67 | 'subscriber_port'], 68 | publisher_port=kwargs[ 69 | 'publisher_port'], 70 | process_name=kwargs[ 71 | 'process_name'], 72 | board_type=kwargs['board_type'] 73 | ) 74 | 75 | # instantiate pymata_rh 76 | self.robohat = pymata_rh.PymataRh(kwargs[ 77 | 'com_port'], kwargs['arduino_instance_id']) 78 | # start the banyan receive loop 79 | try: 80 | self.receive_loop() 81 | except KeyboardInterrupt: 82 | self.clean_up() 83 | sys.exit(0) 84 | 85 | def init_pins_dictionary(self): 86 | """ 87 | The pins dictionary is an array of dictionary items that you create 88 | to describe each GPIO pin. In this dictionary, you can store things 89 | such as the pins current mode, the last value reported for an input pin 90 | callback method for an input pin, etc. 91 | """ 92 | 93 | # not used for robohat gateway, but must be initialized. 94 | self.pins_dictionary = [] 95 | 96 | def additional_banyan_messages(self, topic, payload): 97 | """ 98 | This method will pass any messages not handled by this class to the 99 | specific gateway class. Must be overwritten by the hardware gateway 100 | class. 101 | :param topic: message topic 102 | :param payload: message payload 103 | """ 104 | if payload['command'] == 'initialize_mpu': 105 | self.robohat.mpu_9250_initialize(callback=self.mpu_callback) 106 | elif payload['command'] == 'read_mpu': 107 | self.robohat.mpu_9250_read_data() 108 | elif payload['command'] == 'initialize_ina': 109 | self.robohat.ina_initialize(callback=self.ina_callback) 110 | elif payload['command'] == 'get_ina_bus_voltage': 111 | self.robohat.ina_read_bus_voltage() 112 | elif payload['command'] == 'get_ina_bus_current': 113 | self.robohat.ina_read_bus_current() 114 | elif payload['command'] == 'get_supply_voltage': 115 | self.robohat.ina_read_supply_voltage() 116 | elif payload['command'] == 'get_shunt_voltage': 117 | self.robohat.ina_read_shunt_voltage() 118 | elif payload['command'] == 'get_power': 119 | self.robohat.ina_read_power() 120 | 121 | def digital_write(self, topic, payload): 122 | """ 123 | This method will pass any messages not handled by this class to the 124 | specific gateway class. Must be overwritten by the hardware gateway 125 | class. 126 | :param topic: message topic 127 | :param payload: message payload 128 | """ 129 | self.robohat.digital_write(payload["pin"], payload['value']) 130 | 131 | def disable_analog_reporting(self, topic, payload): 132 | """ 133 | This method will pass any messages not handled by this class to the 134 | specific gateway class. Must be overwritten by the hardware gateway 135 | class. 136 | :param topic: message topic 137 | :param payload: message payload 138 | """ 139 | raise NotImplementedError 140 | 141 | def disable_digital_reporting(self, topic, payload): 142 | """ 143 | This method will pass any messages not handled by this class to the 144 | specific gateway class. Must be overwritten by the hardware gateway 145 | class. 146 | :param topic: message topic 147 | :param payload: message payload 148 | """ 149 | raise NotImplementedError 150 | 151 | def enable_analog_reporting(self, topic, payload): 152 | """ 153 | This method will pass any messages not handled by this class to the 154 | specific gateway class. Must be overwritten by the hardware gateway 155 | class. 156 | :param topic: message topic 157 | :param payload: message payload 158 | """ 159 | raise NotImplementedError 160 | 161 | def enable_digital_reporting(self, topic, payload): 162 | """ 163 | This method will pass any messages not handled by this class to the 164 | specific gateway class. Must be overwritten by the hardware gateway 165 | class. 166 | :param topic: message topic 167 | :param payload: message payload 168 | """ 169 | raise NotImplementedError 170 | 171 | def i2c_read(self, topic, payload): 172 | """ 173 | This method will pass any messages not handled by this class to the 174 | specific gateway class. Must be overwritten by the hardware gateway 175 | class. 176 | :param topic: message topic 177 | :param payload: message payload 178 | """ 179 | raise NotImplementedError 180 | 181 | def i2c_write(self, topic, payload): 182 | """ 183 | This method will pass any messages not handled by this class to the 184 | specific gateway class. Must be overwritten by the hardware gateway 185 | class. 186 | :param topic: message topic 187 | :param payload: message payload 188 | """ 189 | raise NotImplementedError 190 | 191 | def play_tone(self, topic, payload): 192 | """ 193 | This method will pass any messages not handled by this class to the 194 | specific gateway class. Must be overwritten by the hardware gateway 195 | class. 196 | :param topic: message topic 197 | :param payload: message payload 198 | """ 199 | raise NotImplementedError 200 | 201 | def pwm_write(self, topic, payload): 202 | """ 203 | 204 | This method sets the pwm value for the selected pin. 205 | Call set_mode_pwm before calling this method. 206 | :param topic: from_robohat_gui 207 | :param payload: {“command”: “pwm_write”, "pin": “PIN”, 208 | "tag":”TAG”, 209 | “value”: “VALUE”} 210 | """ 211 | self.robohat.pwm_write(payload["pin"], payload['value']) 212 | 213 | def servo_position(self, topic, payload): 214 | """ 215 | This method will pass any messages not handled by this class to the 216 | specific gateway class. Must be overwritten by the hardware gateway 217 | class. 218 | :param topic: message topic 219 | :param payload: message payload 220 | """ 221 | # self.pins_dictionary[pin][GatewayBase.PIN_MODE] = GatewayBase.SERVO_MODE 222 | self.robohat.servo_write(payload["pin"], payload["position"]) 223 | 224 | def set_mode_analog_input(self, topic, payload): 225 | """ 226 | This method will pass any messages not handled by this class to the 227 | specific gateway class. Must be overwritten by the hardware gateway 228 | class. 229 | :param topic: message topic 230 | :param payload: message payload 231 | """ 232 | pin = payload["pin"] 233 | # self.pins_dictionary[pin + self.first_analog_pin][GatewayBase.PIN_MODE] = \ 234 | # GatewayBase.ANALOG_INPUT_MODE 235 | self.robohat.set_pin_mode_analog_input(pin, self.analog_input_callback) 236 | 237 | def set_mode_digital_input(self, topic, payload): 238 | """ 239 | This method will pass any messages not handled by this class to the 240 | specific gateway class. Must be overwritten by the hardware gateway 241 | class. 242 | :param topic: message topic 243 | :param payload: message payload 244 | """ 245 | pin = payload["pin"] 246 | # self.pins_dictionary[pin][GatewayBase.PIN_MODE] = GatewayBase.DIGITAL_INPUT_PULLUP_MODE 247 | self.robohat.set_pin_mode_digital_input_pullup(pin, self.digital_input_callback) 248 | 249 | def set_mode_digital_input_pullup(self, topic, payload): 250 | """ 251 | This method will pass any messages not handled by this class to the 252 | specific gateway class. Must be overwritten by the hardware gateway 253 | class. 254 | :param topic: message topic 255 | :param payload: message payload 256 | """ 257 | raise NotImplementedError 258 | 259 | def set_mode_digital_output(self, topic, payload): 260 | """ 261 | This method will pass any messages not handled by this class to the 262 | specific gateway class. Must be overwritten by the hardware gateway 263 | class. 264 | :param topic: message topic 265 | :param payload: message payload 266 | """ 267 | pin = payload["pin"] 268 | # self.pins_dictionary[pin][GatewayBase.PIN_MODE] = GatewayBase.DIGITAL_OUTPUT_MODE 269 | self.robohat.set_pin_mode_digital_output(pin) 270 | 271 | def set_mode_i2c(self, topic, payload): 272 | """ 273 | This method will pass any messages not handled by this class to the 274 | specific gateway class. Must be overwritten by the hardware gateway 275 | class. 276 | :param topic: message topic 277 | :param payload: message payload 278 | """ 279 | raise NotImplementedError 280 | 281 | def set_mode_pwm(self, topic, payload): 282 | """ 283 | This method will pass any messages not handled by this class to the 284 | specific gateway class. Must be overwritten by the hardware gateway 285 | class. 286 | :param topic: message topic 287 | :param payload: message payload 288 | """ 289 | pin = payload["pin"] 290 | # self.pins_dictionary[pin][GatewayBase.PIN_MODE] = GatewayBase.PWM_OUTPUT_MODE 291 | self.robohat.set_pin_mode_pwm_output(pin) 292 | 293 | def set_mode_servo(self, topic, payload): 294 | """ 295 | This method will pass any messages not handled by this class to the 296 | specific gateway class. Must be overwritten by the hardware gateway 297 | class. 298 | :param topic: message topic 299 | :param payload: message payload 300 | """ 301 | pin = payload["pin"] 302 | # self.pins_dictionary[pin][GatewayBase.PIN_MODE] = GatewayBase.SERVO_MODE 303 | self.robohat.set_pin_mode_servo(pin) 304 | 305 | def set_mode_sonar(self, topic, payload): 306 | """ 307 | This method will pass any messages not handled by this class to the 308 | specific gateway class. Must be overwritten by the hardware gateway 309 | class. 310 | :param topic: message topic 311 | :param payload: message payload 312 | """ 313 | raise NotImplementedError 314 | 315 | def set_mode_stepper(self, topic, payload): 316 | """ 317 | Set the mode for the specific board. 318 | Must be overwritten by the hardware gateway class. 319 | :param topic: message topic 320 | :param payload: message payload 321 | """ 322 | raise NotImplementedError 323 | 324 | def set_mode_tone(self, topic, payload): 325 | """ 326 | Must be overwritten by the hardware gateway class. 327 | Handles a digital write 328 | :param topic: message topic 329 | :param payload: message payload 330 | """ 331 | raise NotImplementedError 332 | 333 | def digital_read(self, pin): 334 | raise NotImplementedError 335 | 336 | def stepper_write(self, topic, payload): 337 | """ 338 | Must be overwritten by the hardware gateway class. 339 | Handles a pwm write 340 | :param topic: message topic 341 | :param payload: message payload 342 | """ 343 | raise NotImplementedError 344 | 345 | def analog_input_callback(self, data): 346 | # data = [pin mode, pin, current reported value, timestamp] 347 | # self.pins_dictionary[data[1] + self.arduino.first_analog_pin][GatewayBase.LAST_VALUE] = data[2] 348 | payload = {'report': 'analog_input', 'pin': data[1], 349 | 'value': data[2], 'timestamp': data[3]} 350 | self.publish_payload(payload, 'from_robohat_gateway') 351 | 352 | def digital_input_callback(self, data): 353 | """ 354 | Digital input data change reported by Arduino 355 | :param data: 356 | :return: 357 | """ 358 | # data = [pin mode, pin, current reported value, timestamp] 359 | # self.pins_dictionary[data[1]][GatewayBase.LAST_VALUE] = data[2] 360 | payload = {'report': 'digital_input', 'pin': data[1], 361 | 'value': data[2], 'timestamp': data[3]} 362 | self.publish_payload(payload, 'from_robohat_gateway') 363 | 364 | def mpu_callback(self, data): 365 | """ 366 | Digital input data change reported by Arduino 367 | :param data: 368 | index[0] = pin type - for mpu9250 the value is 16 369 | index[1] = mpu address 370 | index[2] = accelerometer x axis 371 | index[3] = accelerometer y axis 372 | index[4] = accelerometer z axis 373 | index[5] = gyroscope x axis 374 | index[6] = gyroscope y axis 375 | index[7] = gyroscope z axis 376 | index[8] = magnetometer x axis 377 | index[9] = magnetometer y axis 378 | index[10] = magnetometer z axis 379 | index[11] = temperature 380 | index[12] = timestamp 381 | :return: 382 | """ 383 | # data = [pin mode, pin, current reported value, timestamp] 384 | # self.pins_dictionary[data[1]][GatewayBase.LAST_VALUE] = data[2] 385 | payload = {'report': 'mpu', 386 | 'Ax': data[2], 'Ay': data[3], 'Az': data[4], 387 | 'Gx': data[5], 'Gy': data[6], 'Gz': data[7], 388 | 'Mx': data[8], 'My': data[9], 'Mz': data[10], 389 | 'Temperature': data[11] 390 | } 391 | self.publish_payload(payload, 'from_robohat_gateway') 392 | 393 | def ina_callback(self, data): 394 | payload = {} 395 | cb_reported_value = data[3] 396 | if data[2] == 0: 397 | payload = {'report': 'ina', 'param': 'V', 'value': cb_reported_value} 398 | elif data[2] == 1: 399 | payload = {'report': 'ina', 'param': ' A', 'value': cb_reported_value} 400 | elif data[2] == 2: 401 | payload = {'report': 'ina', 'param': 'Supply', 'value': cb_reported_value} 402 | elif data[2] == 3: 403 | payload = {'report': 'ina', 'param': 'Shunt', 'value': cb_reported_value} 404 | elif data[2] == 4: 405 | payload = {'report': 'ina', 'param': 'Power', 'value': cb_reported_value} 406 | self.publish_payload(payload, 'from_robohat_gateway') 407 | 408 | 409 | def robohat_gateway(): 410 | parser = argparse.ArgumentParser() 411 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 412 | help="None or IP address used by Back Plane") 413 | parser.add_argument("-c", dest="com_port", default="None", 414 | help="Use this COM port instead of auto discovery") 415 | parser.add_argument("-d", dest="board_type", default="None", 416 | help="This parameter identifies the target GPIO " 417 | "device") 418 | parser.add_argument("-i", dest="arduino_instance_id", default="1", 419 | help="Set an Arduino Instance ID and match it in FirmataExpress") 420 | parser.add_argument("-g", dest="log", default="False", 421 | help="Set to True to turn logging on.") 422 | parser.add_argument("-l", dest="subscriber_list", 423 | default="to_robohat_gateway", nargs='+', 424 | help="Banyan topics space delimited: topic1 topic2 " 425 | "topic3") 426 | parser.add_argument("-n", dest="process_name", default="RoboHatGateway", 427 | help="Set process name in banner") 428 | parser.add_argument("-p", dest="publisher_port", default='43124', 429 | help="Publisher IP port") 430 | parser.add_argument("-s", dest="subscriber_port", default='43125', 431 | help="Subscriber IP port") 432 | parser.add_argument("-t", dest="loop_time", default=".1", 433 | help="Event Loop Timer in seconds") 434 | 435 | args = parser.parse_args() 436 | if args.back_plane_ip_address == 'None': 437 | args.back_plane_ip_address = None 438 | if args.board_type == 'None': 439 | args.back_plane_ip_address = None 440 | if args.com_port == 'None': 441 | args.com_port = None 442 | kw_options = { 443 | 'back_plane_ip_address': args.back_plane_ip_address, 444 | 'publisher_port': args.publisher_port, 445 | 'subscriber_port': args.subscriber_port, 446 | 'process_name': args.process_name, 447 | 'loop_time': float(args.loop_time), 448 | 'board_type': args.board_type, 449 | 'com_port': args.com_port, 450 | 'arduino_instance_id': int(args.arduino_instance_id) 451 | } 452 | try: 453 | RoboHatGateway(args.subscriber_list, **kw_options) 454 | except KeyboardInterrupt: 455 | sys.exit() 456 | 457 | 458 | # signal handler function called when Control-C occurs 459 | # noinspection PyShadowingNames,PyUnusedLocal,PyUnusedLocal 460 | def signal_handler(sig, frame): 461 | print('Exiting Through Signal Handler') 462 | raise KeyboardInterrupt 463 | 464 | 465 | # listen for SIGINT 466 | signal.signal(signal.SIGINT, signal_handler) 467 | signal.signal(signal.SIGTERM, signal_handler) 468 | 469 | if __name__ == '__main__': 470 | # replace with name of function you defined above 471 | robohat_gateway() 472 | -------------------------------------------------------------------------------- /s3_extend/gateways/rpi_gateway.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This is the Python Banyan GUI that communicates with 5 | the Raspberry Pi Banyan Gateway 6 | 7 | Copyright (c) 2019 Alan Yorinks All right reserved. 8 | 9 | Python Banyan is free software; you can redistribute it and/or 10 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 11 | Version 3 as published by the Free Software Foundation; either 12 | or (at your option) any later version. 13 | This library is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | General Public License for more details. 17 | 18 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 19 | along with this library; if not, write to the Free Software 20 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 | 22 | """ 23 | import argparse 24 | import signal 25 | from subprocess import run 26 | import sys 27 | import time 28 | 29 | import pigpio 30 | from python_banyan.gateway_base import GatewayBase 31 | 32 | from .sonar import Sonar 33 | from .stepper import StepperMotor 34 | 35 | import zmq 36 | 37 | 38 | # noinspection PyAbstractClass 39 | class RpiGateway(GatewayBase): 40 | """ 41 | This class implements a Banyan gateway for the Raspberry Pi 42 | GPIO pins. It implements the Common Unified GPIO Message 43 | Specification. 44 | 45 | If pipgiod is not currently running, it will start it, and 46 | no backplane ip address was specified, a local backplane 47 | will be automatically started. If you specify a backplane 48 | ip address, you will need to start that backplane manually. 49 | """ 50 | 51 | def __init__(self, *subscriber_list, **kwargs): 52 | """ 53 | Initialize the class for operation 54 | :param subscriber_list: A list of subscription topics 55 | :param kwargs: Command line arguments - see bg4rpi() 56 | at the bottom of this file. 57 | """ 58 | 59 | # attempt to instantiate pigpio 60 | self.pi = pigpio.pi() 61 | 62 | # if pigpiod is not running, try to start it 63 | # 64 | if not self.pi.connected: 65 | print('\nAttempting to start pigpiod...') 66 | run(['sudo', 'pigpiod']) 67 | time.sleep(.5) 68 | self.pi = pigpio.pi() 69 | if not self.pi.connected: 70 | print('pigpiod did not start - goodbye.') 71 | sys.exit() 72 | else: 73 | print('pigpiod successfully started!') 74 | 75 | print('pigpiod is running version: ', self.pi.get_pigpio_version()) 76 | 77 | # variables to hold instances of sonar and stepper 78 | self.sonar = None 79 | self.stepper = None 80 | 81 | # a list of valid pin numbers 82 | # using a list comprehension to create the list 83 | self.gpio_pins = [pin for pin in range(2, 28)] 84 | 85 | # i2c handle supplied by pigpio when i2c open is called 86 | self.i2c_handle = None 87 | 88 | # initialize the parent 89 | super(RpiGateway, self).__init__(subscriber_list=subscriber_list, 90 | back_plane_ip_address=kwargs['back_plane_ip_address'], 91 | subscriber_port=kwargs['subscriber_port'], 92 | publisher_port=kwargs['publisher_port'], 93 | process_name=kwargs['process_name'], 94 | ) 95 | 96 | # start the banyan receive loop 97 | try: 98 | self.receive_loop() 99 | except KeyboardInterrupt: 100 | self.pi.stop() 101 | self.clean_up() 102 | sys.exit(0) 103 | 104 | def init_pins_dictionary(self): 105 | """ 106 | This method is called by the gateway_base parent class 107 | when it is initializing. 108 | """ 109 | 110 | # build a status table for the pins 111 | for x in self.gpio_pins: 112 | entry = {'mode': None, 'duty': None, 'freq': None, 'value': 0} 113 | self.pins_dictionary[x] = entry 114 | 115 | def digital_write(self, topic, payload): 116 | """ 117 | This method performs a digital write 118 | :param topic: message topic 119 | :param payload: {"command": "digital_write", "pin": “PIN”, "value": “VALUE”} 120 | """ 121 | self.pi.write(payload['pin'], payload['value']) 122 | 123 | def disable_digital_reporting(self, topic, payload): 124 | """ 125 | This method disables digital input reporting for the selected pin. 126 | 127 | :param topic: message topic 128 | :param payload: {"command": "disable_digital_reporting", "pin": “PIN”, "tag": "TAG"} 129 | """ 130 | entry = self.pins_dictionary[payload['pin']] 131 | entry['mode'] = None 132 | 133 | def i2c_read(self, topic, payload): 134 | """ 135 | This method will perform an i2c read by specifying the i2c 136 | device address, i2c device register and the number of bytes 137 | to read. 138 | 139 | Call set_mode_i2c first to establish the pins for i2c operation. 140 | 141 | :param topic: message topic 142 | :param payload: {"command": "i2c_read", "pin": “PIN”, "tag": "TAG", 143 | "addr": “I2C ADDRESS, "register": “I2C REGISTER”, 144 | "number_of_bytes": “NUMBER OF BYTES”} 145 | :return via the i2c_callback method 146 | """ 147 | 148 | # if no i2c handle has been assigned, get one 149 | if self.i2c_handle is None: 150 | self.i2c_handle = self.pi.i2c_open(1, payload['addr'], 0) 151 | 152 | # using the handle do the i2c read and retrieve the data 153 | value = self.pi.i2c_read_i2c_block_data(self.i2c_handle, 154 | payload['register'], 155 | payload['number_of_bytes']) 156 | # value index 0 is the number of bytes read. 157 | # the remainder of value is the a byte array of data returned 158 | # convert the data to a list 159 | value = list(value[1]) 160 | 161 | # format the data for report to be published containing 162 | # the data received 163 | report = ', '.join([str(elem) for elem in value]) 164 | payload = {'report': 'i2c_data', 'value': report} 165 | self.publish_payload(payload, 'from_rpi_gateway') 166 | 167 | def i2c_write(self, topic, payload): 168 | """ 169 | This method will perform an i2c write for the i2c device with 170 | the specified i2c device address, i2c register and a list of byte 171 | to write. 172 | 173 | Call set_mode_i2c first to establish the pins for i2c operation. 174 | 175 | :param topic: message topic 176 | :param payload: {"command": "i2c_write", "pin": “PIN”, "tag": "TAG", 177 | "addr": “I2C ADDRESS, "register": “I2C REGISTER”, 178 | "data": [“DATA IN LIST FORM”]} 179 | """ 180 | 181 | if self.i2c_handle is None: 182 | self.i2c_handle = self.pi.i2c_open(1, payload['addr'], 0) 183 | 184 | data = payload['data'] 185 | 186 | self.pi.i2c_write_device(self.i2c_handle, data) 187 | 188 | # give the i2c device some time to process the write request 189 | time.sleep(.4) 190 | 191 | def set_mode_tone(self, topic, payload): 192 | pass 193 | 194 | def play_tone(self, topic, payload): 195 | """ 196 | This method plays a tone on a piezo device connected to the selected 197 | pin at the frequency and duration requested. 198 | Frequency is in hz and duration in milliseconds. 199 | 200 | Call set_mode_tone before using this method. 201 | :param topic: message topic 202 | :param payload: {"command": "play_tone", "pin": “PIN”, "tag": "TAG", 203 | “freq”: ”FREQUENCY”, duration: “DURATION”} 204 | """ 205 | pin = int(payload['pin']) 206 | self.pi.set_mode(pin, pigpio.OUTPUT) 207 | 208 | frequency = int(payload['freq']) 209 | if frequency < 0: 210 | frequency = 0 211 | if frequency != 0: 212 | frequency = int((1000 / frequency) * 1000) 213 | tone = [pigpio.pulse(1 << pin, 0, frequency // 2), 214 | pigpio.pulse(0, 1 << pin, frequency // 2)] 215 | 216 | self.pi.wave_add_generic(tone) 217 | wid = self.pi.wave_create() 218 | 219 | if wid >= 0: 220 | self.pi.wave_send_repeat(wid) 221 | time.sleep(payload['duration'] / 1000) 222 | self.pi.wave_tx_stop() 223 | self.pi.wave_delete(wid) 224 | 225 | def pwm_write(self, topic, payload): 226 | """ 227 | This method sets the pwm value for the selected pin. 228 | Call set_mode_pwm before calling this method. 229 | :param topic: message topic 230 | :param payload: {“command”: “pwm_write”, "pin": “PIN”, 231 | "tag":”TAG”, 232 | “value”: “VALUE”} 233 | """ 234 | value = payload['value'] 235 | if value < 0: 236 | value = 0 237 | elif payload['value'] > 255: 238 | value = 255 239 | 240 | self.pi.set_PWM_dutycycle(payload['pin'], value) 241 | 242 | def servo_position(self, topic, payload): 243 | """ 244 | This method will set a servo's position in degrees. 245 | Call set_mode_servo first to activate the pin for 246 | servo operation. 247 | 248 | :param topic: message topic 249 | :param payload: {'command': 'servo_position', 250 | "pin": “PIN”,'tag': 'servo', 251 | “position”: “POSITION”} 252 | """ 253 | pin = payload['pin'] 254 | pulse_width = (payload['position'] * 10) + 600 255 | self.pi.set_servo_pulsewidth(pin, pulse_width) 256 | 257 | def set_mode_analog_input(self, topic, payload): 258 | """ 259 | This method programs a PCF8591 AD/DA for analog input. 260 | :param topic: message topic 261 | :param payload: {"command": "set_mode_analog_input", 262 | "pin": “PIN”, "tag":”TAG” } 263 | """ 264 | 265 | # pin is used as channel number 266 | 267 | value = None 268 | i2c_handle = self.pi.i2c_open(1, 72, 0) 269 | pin = payload['pin'] 270 | 271 | self.pi.i2c_write_byte_data(i2c_handle, 64 | (pin & 0x03), 0) 272 | time.sleep(0.1) 273 | for i in range(3): 274 | value = self.pi.i2c_read_byte(i2c_handle) 275 | 276 | self.pi.i2c_close(i2c_handle) 277 | 278 | # publish an analog input report 279 | payload = {'report': 'analog_input', 'pin': pin, 280 | 'value': value} 281 | self.publish_payload(payload, 'from_rpi_gateway') 282 | 283 | def set_mode_digital_input(self, topic, payload): 284 | """ 285 | This method sets a pin as digital input. 286 | :param topic: message topic 287 | :param payload: {"command": "set_mode_digital_input", "pin": “PIN”, "tag":”TAG” } 288 | """ 289 | pin = payload['pin'] 290 | entry = self.pins_dictionary[pin] 291 | entry['mode'] = self.DIGITAL_INPUT_MODE 292 | 293 | self.pi.set_glitch_filter(pin, 20000) 294 | self.pi.set_mode(pin, pigpio.INPUT) 295 | self.pi.set_pull_up_down(pin, pigpio.PUD_DOWN) 296 | 297 | self.pi.callback(pin, pigpio.EITHER_EDGE, self.input_callback) 298 | 299 | def set_mode_digital_output(self, topic, payload): 300 | """ 301 | This method sets a pin as a digital output pin. 302 | :param topic: message topic 303 | :param payload: {"command": "set_mode_digital_output", 304 | "pin": PIN, "tag":”TAG” } 305 | """ 306 | self.pi.set_mode(payload['pin'], pigpio.OUTPUT) 307 | 308 | def set_mode_pwm(self, topic, payload): 309 | """ 310 | This method sets a GPIO pin capable of PWM for PWM operation. 311 | :param topic: message topic 312 | :param payload: {"command": "set_mode_pwm", "pin": “PIN”, "tag":”TAG” } 313 | """ 314 | self.pi.set_mode(payload['pin'], pigpio.OUTPUT) 315 | 316 | def set_mode_servo(self, topic, payload): 317 | """ 318 | This method establishes a GPIO pin for servo operation. 319 | :param topic: message topic 320 | :param payload: {"command": "set_mode_servo", "pin": “PIN”, "tag":”TAG” } 321 | """ 322 | pass 323 | 324 | def set_mode_sonar(self, topic, payload): 325 | """ 326 | This method sets the trigger and echo pins for sonar operation. 327 | :param topic: message topic 328 | :param payload: {"command": "set_mode_sonar", "trigger_pin": “PIN”, "tag":”TAG” 329 | "echo_pin": “PIN”"tag":”TAG” } 330 | """ 331 | trigger = payload['trigger_pin'] 332 | echo = payload['echo_pin'] 333 | self.sonar = Sonar(self.pi, trigger, echo) 334 | self.receive_loop_idle_addition = self.read_sonar 335 | 336 | def read_sonar(self): 337 | """ 338 | Read the sonar device and convert value to 339 | centimeters. The value is then published as a report. 340 | """ 341 | sonar_time = self.sonar.read() 342 | distance = sonar_time / 29 / 2 343 | distance = round(distance, 2) 344 | payload = {'report': 'sonar_data', 'value': distance} 345 | self.publish_payload(payload, 'from_rpi_gateway') 346 | 347 | def set_mode_stepper(self, topic, payload): 348 | """ 349 | This method establishes either 2 or 4 GPIO pins to be used in stepper 350 | motor operation. 351 | :param topic: 352 | :param payload:{"command": "set_mode_stepper", "pins": [“PINS”], 353 | "steps_per_revolution": “NUMBER OF STEPS”} 354 | """ 355 | self.stepper = StepperMotor(self.pi, payload['pins'][0], 356 | payload['pins'][1], 357 | payload['pins'][2], 358 | payload['pins'][3]) 359 | 360 | def stepper_write(self, topic, payload): 361 | """ 362 | Move a stepper motor for the specified number of steps. 363 | :param topic: 364 | :param payload: {"command": "stepper_write", "motor_speed": “SPEED”, 365 | "number_of_steps":”NUMBER OF STEPS” } 366 | """ 367 | if not self.stepper: 368 | raise RuntimeError('Stepper was not initialized') 369 | 370 | number_of_steps = payload['number_of_steps'] 371 | if number_of_steps >= 0: 372 | for i in range(number_of_steps): 373 | self.stepper.do_clockwise_step() 374 | else: 375 | for i in range(abs(number_of_steps)): 376 | self.stepper.do_counterclockwise_step() 377 | 378 | def input_callback(self, pin, level, tick): 379 | """ 380 | This method is called by pigpio when it detects a change for 381 | a digital input pin. A report is published reflecting 382 | the change of pin state for the pin. 383 | :param pin: 384 | :param level: 385 | :param tick: 386 | :return: 387 | """ 388 | # payload = {'report': 'digital_input_change', 'pin': str(pin), 'level': str(level)} 389 | entry = self.pins_dictionary[pin] 390 | if entry['mode'] == self.DIGITAL_INPUT_MODE: 391 | payload = {'report': 'digital_input', 'pin': pin, 392 | 'value': level, 'timestamp': time.time()} 393 | self.publish_payload(payload, 'from_rpi_gateway') 394 | 395 | 396 | def rpi_gateway(): 397 | parser = argparse.ArgumentParser() 398 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 399 | help="None or IP address used by Back Plane") 400 | parser.add_argument("-m", dest="subscriber_list", default="to_rpi_gateway", nargs='+', 401 | help="Banyan topics space delimited: topic1 topic2 topic3") 402 | parser.add_argument("-n", dest="process_name", default="RaspberryPiGateway", 403 | help="Set process name in banner") 404 | parser.add_argument("-p", dest="publisher_port", default='43124', 405 | help="Publisher IP port") 406 | parser.add_argument("-s", dest="subscriber_port", default='43125', 407 | help="Subscriber IP port") 408 | parser.add_argument("-t", dest="loop_time", default=".1", 409 | help="Event Loop Timer in seconds") 410 | 411 | args = parser.parse_args() 412 | if args.back_plane_ip_address == 'None': 413 | args.back_plane_ip_address = None 414 | kw_options = { 415 | 'back_plane_ip_address': args.back_plane_ip_address, 416 | 'publisher_port': args.publisher_port, 417 | 'subscriber_port': args.subscriber_port, 418 | 'process_name': args.process_name, 419 | 'loop_time': float(args.loop_time)} 420 | 421 | try: 422 | RpiGateway(args.subscriber_list, **kw_options) 423 | except KeyboardInterrupt: 424 | sys.exit() 425 | 426 | 427 | def signal_handler(sig, frame): 428 | print('Exiting Through Signal Handler') 429 | raise KeyboardInterrupt 430 | 431 | 432 | # listen for SIGINT 433 | signal.signal(signal.SIGINT, signal_handler) 434 | signal.signal(signal.SIGTERM, signal_handler) 435 | 436 | 437 | if __name__ == '__main__': 438 | # replace with name of function you defined above 439 | rpi_gateway() 440 | -------------------------------------------------------------------------------- /s3_extend/gateways/servo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # servo_demo.py 4 | # 2016-10-07 5 | # Public Domain 6 | 7 | # servo_demo.py # Send servo pulses to GPIO 4. 8 | # servo_demo.py 23 24 25 # Send servo pulses to GPIO 23, 24, 25. 9 | 10 | import sys 11 | import time 12 | import random 13 | import pigpio 14 | 15 | NUM_GPIO = 32 16 | 17 | MIN_WIDTH = 500 18 | MAX_WIDTH = 2500 19 | 20 | step = [0] * NUM_GPIO 21 | width = [0] * NUM_GPIO 22 | used = [False] * NUM_GPIO 23 | 24 | pi = pigpio.pi() 25 | 26 | if not pi.connected: 27 | exit() 28 | 29 | G = [4] 30 | 31 | 32 | for g in G: 33 | used[g] = True 34 | step[g] = random.randrange(50, 55) 35 | if step[g] % 2 == 0: 36 | step[g] = -step[g] 37 | width[g] = random.randrange(MIN_WIDTH, MAX_WIDTH + 1) 38 | 39 | print("Sending servos pulses to GPIO {}, control C to stop.". 40 | format(' '.join(str(g) for g in G))) 41 | 42 | while True: 43 | 44 | try: 45 | 46 | for g in G: 47 | 48 | pi.set_servo_pulsewidth(g, width[g]) 49 | 50 | # print(g, width[g]) 51 | 52 | width[g] += step[g] 53 | 54 | if width[g] < MIN_WIDTH or width[g] > MAX_WIDTH: 55 | step[g] = -step[g] 56 | width[g] += step[g] 57 | 58 | time.sleep(0.01) 59 | 60 | except KeyboardInterrupt: 61 | break 62 | 63 | print("\nTidying up") 64 | 65 | for g in G: 66 | pi.set_servo_pulsewidth(g, 0) 67 | 68 | pi.stop() 69 | -------------------------------------------------------------------------------- /s3_extend/gateways/sonar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pigpio 4 | import time 5 | 6 | # based on an example provided with the pigpio library 7 | 8 | class Sonar: 9 | """ 10 | This class encapsulates a sonar device with separate 11 | trigger and echo pins. 12 | 13 | A pulse on the trigger initiates the sonar ping and shortly 14 | afterwards a sonar pulse is transmitted and the echo pin 15 | goes high. The echo pins stays high until a sonar echo is 16 | received (or the response times-out). The time between 17 | the high and low edges indicates the sonar round trip time. 18 | """ 19 | 20 | def __init__(self, pi, trigger, echo): 21 | """ 22 | The class is instantiated with the Pi to use and the 23 | gpios connected to the trigger and echo pins. 24 | """ 25 | self.pi = pi 26 | self._trig = trigger 27 | self._echo = echo 28 | 29 | self._ping = False 30 | self._high = None 31 | self._time = None 32 | 33 | self._triggered = False 34 | 35 | self._trig_mode = pi.get_mode(self._trig) 36 | self._echo_mode = pi.get_mode(self._echo) 37 | 38 | pi.set_mode(self._trig, pigpio.OUTPUT) 39 | pi.set_mode(self._echo, pigpio.INPUT) 40 | 41 | self._cb = pi.callback(self._trig, pigpio.EITHER_EDGE, self._cbf) 42 | self._cb = pi.callback(self._echo, pigpio.EITHER_EDGE, self._cbf) 43 | 44 | self._inited = True 45 | 46 | def _cbf(self, gpio, level, tick): 47 | if gpio == self._trig: 48 | if level == 0: # trigger sent 49 | self._triggered = True 50 | self._high = None 51 | else: 52 | if self._triggered: 53 | if level == 1: 54 | self._high = tick 55 | else: 56 | if self._high is not None: 57 | self._time = tick - self._high 58 | self._high = None 59 | self._ping = True 60 | 61 | def read(self): 62 | """ 63 | Triggers a reading. The returned reading is the number 64 | of microseconds for the sonar round-trip. 65 | 66 | round trip cms = round trip time / 1000000.0 * 34030 67 | """ 68 | if self._inited: 69 | self._ping = False 70 | self.pi.gpio_trigger(self._trig) 71 | start = time.time() 72 | while not self._ping: 73 | if (time.time() - start) > 5.0: 74 | return 20000 75 | time.sleep(0.001) 76 | return self._time 77 | else: 78 | return None 79 | 80 | def cancel(self): 81 | """ 82 | Cancels the ranger and returns the gpios to their 83 | original mode. 84 | """ 85 | if self._inited: 86 | self._inited = False 87 | self._cb.cancel() 88 | self.pi.set_mode(self._trig, self._trig_mode) 89 | self.pi.set_mode(self._echo, self._echo_mode) 90 | 91 | 92 | # if __name__ == "__main__": 93 | # 94 | # import time 95 | # 96 | # import pigpio 97 | # 98 | # import sonar_trigger_echo 99 | # 100 | # pi = pigpio.pi() 101 | # 102 | # sonar = sonar_trigger_echo.Sonar(pi, 23, 18) 103 | # 104 | # end = time.time() + 600.0 105 | # 106 | # r = 1 107 | # while time.time() < end: 108 | # stime = sonar.read() 109 | # distance = stime/ 29 / 2 110 | # distance = round(distance, 2) 111 | # print("{} {}".format(r, distance)) 112 | # r += 1 113 | # time.sleep(0.03) 114 | # 115 | # sonar.cancel() 116 | # 117 | # pi.stop() 118 | -------------------------------------------------------------------------------- /s3_extend/gateways/stepper.py: -------------------------------------------------------------------------------- 1 | # modified from https://github.com/stripcode/pigpio-stepper-motor 2 | 3 | from collections import deque 4 | from time import sleep 5 | import pigpio 6 | 7 | fullStepSequence = ( 8 | (1, 0, 0, 0), 9 | (0, 1, 0, 0), 10 | (0, 0, 1, 0), 11 | (0, 0, 0, 1) 12 | ) 13 | 14 | halfStepSequence = ( 15 | (1, 0, 0, 0), 16 | (1, 1, 0, 0), 17 | (0, 1, 0, 0), 18 | (0, 1, 1, 0), 19 | (0, 0, 1, 0), 20 | (0, 0, 1, 1), 21 | (0, 0, 0, 1), 22 | (1, 0, 0, 1) 23 | ) 24 | 25 | 26 | class StepperMotor: 27 | 28 | def __init__(self, pi, pin1, pin2, pin3, pin4, sequence=fullStepSequence, delay_after_step=0.0025): 29 | if not isinstance(pi, pigpio.pi): 30 | raise TypeError("Pigpio instance not found.") 31 | self.pi = pi 32 | 33 | self.pi.set_mode(pin1, pigpio.OUTPUT) 34 | self.pi.set_mode(pin2, pigpio.OUTPUT) 35 | self.pi.set_mode(pin3, pigpio.OUTPUT) 36 | self.pi.set_mode(pin4, pigpio.OUTPUT) 37 | self.pin1 = pin1 38 | self.pin2 = pin2 39 | self.pin3 = pin3 40 | self.pin4 = pin4 41 | self.delay_after_step = delay_after_step 42 | self.deque = deque(sequence) 43 | 44 | def do_counterclockwise_step(self): 45 | self.deque.rotate(-1) 46 | self.do_step_and_delay(self.deque[0]) 47 | 48 | def do_clockwise_step(self): 49 | self.deque.rotate(1) 50 | self.do_step_and_delay(self.deque[0]) 51 | 52 | def do_step_and_delay(self, step): 53 | self.pi.write(self.pin1, step[0]) 54 | self.pi.write(self.pin2, step[1]) 55 | self.pi.write(self.pin3, step[2]) 56 | self.pi.write(self.pin4, step[3]) 57 | sleep(self.delay_after_step) 58 | 59 | 60 | # from stepit import StepperMotor 61 | 62 | 63 | -------------------------------------------------------------------------------- /s3_extend/gateways/ws_gateway.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Web Socket Gateway 5 | 6 | Copyright (c) 2019-2024 Alan Yorinks All right reserved. 7 | 8 | Python Banyan is free software; you can redistribute it and/or 9 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 10 | Version 3 as published by the Free Software Foundation; either 11 | or (at your option) any later version. 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | General Public License for more details. 16 | 17 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 18 | along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | 21 | """ 22 | 23 | import argparse 24 | import asyncio 25 | import datetime 26 | import json 27 | import logging 28 | import pathlib 29 | import signal 30 | import sys 31 | import websockets 32 | 33 | from python_banyan.banyan_base_aio import BanyanBaseAIO 34 | 35 | 36 | class WsGateway(BanyanBaseAIO): 37 | """ 38 | This class is a gateway between a websocket client and the 39 | Banyan network. 40 | 41 | NOTE: This class requires Python 3.7 or above. 42 | 43 | It implements a websocket server. A websocket client, upon 44 | connection, must send an id message e.g.: {"id": "to_arduino"}. 45 | 46 | The id will be used as the topic to publish data to the banyan 47 | network. 48 | """ 49 | 50 | def __init__(self, subscription_list, back_plane_ip_address=None, 51 | subscriber_port='43125', 52 | publisher_port='43124', process_name='WebSocketGateway', 53 | event_loop=None, server_ip_port=9000, log=False): 54 | """ 55 | These are all the normal base class parameters 56 | :param subscription_list: 57 | :param back_plane_ip_address: 58 | :param subscriber_port: 59 | :param publisher_port: 60 | :param process_name: 61 | :param event_loop: 62 | """ 63 | 64 | # set up logging if requested 65 | self.log = log 66 | self.event_loop = event_loop 67 | 68 | # a kludge to shutdown the socket on control C 69 | self.wsocket = None 70 | 71 | if self.log: 72 | fn = str(pathlib.Path.home()) + "/wsgw.log" 73 | self.logger = logging.getLogger(__name__) 74 | logging.basicConfig(filename=fn, filemode='w', level=logging.DEBUG) 75 | sys.excepthook = self.my_handler 76 | 77 | # initialize the base class 78 | super(WsGateway, self).__init__(subscriber_list=subscription_list, 79 | back_plane_ip_address=back_plane_ip_address, 80 | subscriber_port=subscriber_port, 81 | publisher_port=publisher_port, 82 | process_name=process_name, 83 | event_loop=self.event_loop) 84 | 85 | # save the server port number 86 | self.server_ip_port = server_ip_port 87 | 88 | # array of active sockets 89 | self.active_sockets = [] 90 | try: 91 | self.start_server = websockets.serve(self.wsg, 92 | '0.0.0.0', 93 | self.server_ip_port) 94 | print('WebSocket using: ' + self.back_plane_ip_address 95 | + ':' + self.server_ip_port) 96 | # start the websocket server and call the main task, wsg 97 | self.event_loop.run_until_complete(self.start_server) 98 | self.event_loop.create_task(self.wakeup()) 99 | self.event_loop.run_forever() 100 | except (websockets.exceptions.ConnectionClosed, 101 | RuntimeError, 102 | KeyboardInterrupt): 103 | if self.log: 104 | logging.exception("Exception occurred", exc_info=True) 105 | self.event_loop.stop() 106 | self.event_loop.close() 107 | 108 | async def wakeup(self): 109 | while True: 110 | try: 111 | await asyncio.sleep(1) 112 | except KeyboardInterrupt: 113 | for task in asyncio.Task.all_tasks(): 114 | task.cancel() 115 | await self.wsocket.close() 116 | self.event_loop.stop() 117 | self.event_loop.close() 118 | sys.exit(0) 119 | 120 | async def wsg(self, websocket, path): 121 | """ 122 | This method handles connections and will be used to send 123 | messages to the client 124 | :param websocket: websocket for connected client 125 | :param path: required, but unused 126 | :return: 127 | """ 128 | self.wsocket = websocket 129 | # start up banyan 130 | await self.begin() 131 | 132 | # wait for a connection 133 | try: 134 | data = await websocket.recv() 135 | except websockets.exceptions.ConnectionClosedOK: 136 | pass 137 | 138 | # expecting an id string from client 139 | data = json.loads(data) 140 | 141 | # if id field not present then raise an exception 142 | try: 143 | id_string = data['id'] 144 | except KeyError: 145 | print('Client did not provide an ID string') 146 | raise 147 | 148 | # create a subscriber string from the id 149 | subscriber_string = id_string.replace('to', 'from') 150 | 151 | # subscribe to that topic 152 | await self.set_subscriber_topic(subscriber_string) 153 | 154 | # add an entry into the active_sockets table 155 | entry = {websocket: 'to_banyan_topic', subscriber_string: websocket} 156 | self.active_sockets.append(entry) 157 | 158 | # create a task to receive messages from the client 159 | await asyncio.create_task(self.receive_data(websocket, data['id'])) 160 | 161 | async def receive_data(self, websocket, publisher_topic): 162 | """ 163 | This method processes a received WebSocket command message 164 | and translates it to a Banyan command message. 165 | :param websocket: The currently active websocket 166 | :param publisher_topic: The publishing topic 167 | """ 168 | while True: 169 | try: 170 | data = await websocket.recv() 171 | data = json.loads(data) 172 | except (websockets.exceptions.ConnectionClosed, TypeError): 173 | # remove the entry from active_sockets 174 | # using a list comprehension 175 | self.active_sockets = [entry for entry in self.active_sockets if 176 | websocket not in entry] 177 | break 178 | 179 | await self.publish_payload(data, publisher_topic) 180 | 181 | async def incoming_message_processing(self, topic, payload): 182 | """ 183 | This method converts the incoming messages to ws messages 184 | and sends them to the ws client 185 | 186 | :param topic: Message Topic string. 187 | 188 | :param payload: Message Data. 189 | """ 190 | if payload['report'] == 'panic': 191 | # close the sockets if in panic mode 192 | for socket in self.active_sockets: 193 | if topic in socket.keys(): 194 | pub_socket = socket[topic] 195 | await pub_socket.close() 196 | 197 | if 'timestamp' in payload: 198 | timestamp = datetime.datetime.fromtimestamp(payload['timestamp']).strftime( 199 | '%Y-%m-%d %H:%M:%S') 200 | payload['timestamp'] = timestamp 201 | 202 | ws_data = json.dumps(payload) 203 | 204 | # find the websocket of interest by looking for the topic in 205 | # active_sockets 206 | for socket in self.active_sockets: 207 | if topic in socket.keys(): 208 | pub_socket = socket[topic] 209 | await pub_socket.send(ws_data) 210 | 211 | def my_handler(self, the_type, value, tb): 212 | """ 213 | For logging uncaught exceptions 214 | :param the_type: 215 | :param value: 216 | :param tb: 217 | :return: 218 | """ 219 | self.logger.exception("Uncaught exception: {0}".format(str(value))) 220 | 221 | 222 | def ws_gateway(): 223 | # allow user to bypass the IP address auto-discovery. This is necessary if the component resides on a computer 224 | # other than the computing running the backplane. 225 | 226 | parser = argparse.ArgumentParser() 227 | parser.add_argument("-b", dest="back_plane_ip_address", default="None", 228 | help="None or IP address used by Back Plane") 229 | # allow the user to specify a name for the component and have it shown on the console banner. 230 | # modify the default process name to one you wish to see on the banner. 231 | # change the default in the derived class to set the name 232 | parser.add_argument("-m", dest="subscription_list", default="from_arduino_gateway, " 233 | "from_esp8266_gateway, " 234 | "from_rpi_gateway, " 235 | "from_microbit_gateway" 236 | "from_picoboard_gateway" 237 | "from_cpx_gateway", 238 | nargs='+', 239 | help="A space delimited list of topics") 240 | parser.add_argument("-i", dest="server_ip_port", default="9000", 241 | help="Set the WebSocket Server IP Port number") 242 | parser.add_argument("-l", dest="log", default="False", 243 | help="Set to True to turn logging on.") 244 | parser.add_argument("-n", dest="process_name", default="WebSocket Gateway", 245 | help="Set process name in banner") 246 | parser.add_argument("-p", dest="publisher_port", default='43124', 247 | help="Publisher IP port") 248 | parser.add_argument("-s", dest="subscriber_port", default='43125', 249 | help="Subscriber IP port") 250 | 251 | args = parser.parse_args() 252 | 253 | subscription_list = args.subscription_list 254 | 255 | if len(subscription_list) > 1: 256 | subscription_list = args.subscription_list.split(',') 257 | 258 | kw_options = { 259 | 'publisher_port': args.publisher_port, 260 | 'subscriber_port': args.subscriber_port, 261 | 'process_name': args.process_name, 262 | 'server_ip_port': args.server_ip_port, 263 | } 264 | 265 | log = args.log.lower() 266 | if log == 'false': 267 | log = False 268 | else: 269 | log = True 270 | 271 | if args.back_plane_ip_address != 'None': 272 | kw_options['back_plane_ip_address'] = args.back_plane_ip_address 273 | 274 | # get the event loop 275 | # this is for python 3.8 276 | if sys.platform == 'win32': 277 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 278 | 279 | loop = asyncio.new_event_loop() 280 | asyncio.set_event_loop(loop) 281 | 282 | try: 283 | WsGateway(subscription_list, **kw_options, event_loop=loop) 284 | except KeyboardInterrupt: 285 | for task in asyncio.Task.all_tasks(): 286 | task.cancel() 287 | loop.stop() 288 | loop.close() 289 | sys.exit(0) 290 | 291 | 292 | def signal_handler(sig, frame): 293 | print('Exiting Through Signal Handler') 294 | raise KeyboardInterrupt 295 | 296 | 297 | # listen for SIGINT 298 | signal.signal(signal.SIGINT, signal_handler) 299 | signal.signal(signal.SIGTERM, signal_handler) 300 | 301 | if __name__ == '__main__': 302 | ws_gateway() 303 | -------------------------------------------------------------------------------- /s3_extend/s32.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | 18 | import atexit 19 | import psutil 20 | import signal 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | # import webbrowser 26 | 27 | 28 | class S32: 29 | """ 30 | This class starts the Banyan server to support the Scratch 3 OneGPIO ESP-8266 31 | extension. 32 | 33 | It will start the backplane, ESP-8266 gateway and websocket gateway. 34 | The wait for IP address flag (-w) is set. 35 | """ 36 | 37 | def __init__(self): 38 | """ 39 | Launch the esp extension 40 | """ 41 | 42 | self.proc_bp = None 43 | self.proc_awg = None 44 | self.proc_hwg = None 45 | 46 | self.skip_backplane = False 47 | 48 | # start backplane 49 | self.proc_bp = self.start_backplane() 50 | if self.proc_bp: 51 | print('backplane started') 52 | 53 | else: 54 | print('backplane start failed - exiting') 55 | sys.exit(0) 56 | 57 | self.proc_awg = self.start_wsgw() 58 | if self.proc_awg: 59 | print('Websocket Gateway started') 60 | else: 61 | print('WebSocket Gateway start failed - exiting') 62 | sys.exit(0) 63 | 64 | # start esp gateway 65 | self.proc_hwg = self.start_esp32gw() 66 | if self.proc_hwg: 67 | print('ESP-32 Gateway started ') 68 | print('To exit this program, press Control-c') 69 | 70 | else: 71 | print('ESP-32 Gateway start failed - exiting') 72 | sys.exit(0) 73 | 74 | atexit.register(self.killall) 75 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 76 | 77 | while True: 78 | try: 79 | if not self.skip_backplane: 80 | if self.proc_bp.poll() is not None: 81 | self.proc_bp = None 82 | print('backplane exited...') 83 | self.killall() 84 | if self.proc_awg.poll() is not None: 85 | self.proc_awg = None 86 | print('Websocket Gateway exited...') 87 | self.killall() 88 | if self.proc_hwg.poll() is not None: 89 | self.proc_hwg = None 90 | print('ESP-8266 Gateway exited.') 91 | self.killall() 92 | 93 | # allow some time between polls 94 | time.sleep(.4) 95 | except Exception as e: 96 | # self.killall() 97 | sys.exit(0) 98 | 99 | def killall(self): 100 | """ 101 | Kill all running processes 102 | """ 103 | 104 | # check for missing processes 105 | if self.proc_bp: 106 | try: 107 | if sys.platform.startswith('win32'): 108 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 109 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 110 | subprocess.CREATE_NO_WINDOW 111 | ) 112 | else: 113 | self.proc_bp.kill() 114 | self.proc_bp = None 115 | except: 116 | pass 117 | if self.proc_awg: 118 | try: 119 | if sys.platform.startswith('win32'): 120 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 121 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 122 | subprocess.CREATE_NO_WINDOW 123 | ) 124 | else: 125 | self.proc_awg.kill() 126 | self.proc_awg = None 127 | except: 128 | pass 129 | if self.proc_hwg: 130 | try: 131 | if sys.platform.startswith('win32'): 132 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 133 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 134 | subprocess.CREATE_NO_WINDOW 135 | ) 136 | else: 137 | self.proc_hwg.kill() 138 | self.proc_hwg = None 139 | except: 140 | pass 141 | # time.sleep(1) 142 | # sys.exit(0) 143 | 144 | def start_backplane(self): 145 | """ 146 | Start the backplane 147 | """ 148 | 149 | # check to see if the backplane is already running 150 | try: 151 | for proc in psutil.process_iter(attrs=['pid', 'name']): 152 | if 'backplane' in proc.info['name']: 153 | self.skip_backplane = True 154 | # its running - return its pid 155 | return proc 156 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 157 | pass 158 | 159 | # backplane is not running, so start one 160 | if sys.platform.startswith('win32'): 161 | return subprocess.Popen(['backplane'], 162 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 163 | subprocess.CREATE_NO_WINDOW) 164 | else: 165 | return subprocess.Popen(['backplane'], 166 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 167 | stdout=subprocess.PIPE) 168 | 169 | def start_wsgw(self): 170 | """ 171 | Start the websocket gateway 172 | """ 173 | if sys.platform.startswith('win32'): 174 | return subprocess.Popen(['wsgw', '-i', '9007'], 175 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 176 | | 177 | subprocess.CREATE_NO_WINDOW) 178 | else: 179 | return subprocess.Popen(['wsgw', '-i', '9007'], 180 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 181 | stdout=subprocess.PIPE) 182 | 183 | def start_esp32gw(self): 184 | """ 185 | Start the esp_8266 gateway 186 | """ 187 | if sys.platform.startswith('win32'): 188 | return subprocess.Popen(['esp32gw'], 189 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 190 | subprocess.CREATE_NO_WINDOW) 191 | else: 192 | return subprocess.Popen(['esp32gw'], stdin=subprocess.PIPE, 193 | stderr=subprocess.PIPE, 194 | stdout=subprocess.PIPE) 195 | 196 | 197 | def signal_handler(sig, frame): 198 | print('Exiting Through Signal Handler') 199 | # raise KeyboardInterrupt 200 | 201 | 202 | def s32ex(): 203 | 204 | # instantiate 205 | S32() 206 | 207 | 208 | # listen for SIGINT 209 | signal.signal(signal.SIGINT, signal_handler) 210 | signal.signal(signal.SIGTERM, signal_handler) 211 | 212 | if __name__ == '__main__': 213 | # replace with name of function you defined above 214 | s32ex() 215 | -------------------------------------------------------------------------------- /s3_extend/s3a.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | 15 | along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | """ 18 | import argparse 19 | import atexit 20 | import signal 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | import psutil 26 | 27 | 28 | # import webbrowser 29 | 30 | 31 | class S3A: 32 | """ 33 | This class starts the Banyan server to support Scratch 3 OneGPIO 34 | for the Arduino 35 | 36 | It will start the backplane, arduino gateway and websocket gateway. 37 | """ 38 | 39 | def __init__(self, com_port=None, arduino_instance_id=None): 40 | """ 41 | 42 | :param com_port: 43 | :param arduino_instance_id: 44 | """ 45 | 46 | self.com_port = com_port 47 | 48 | # psutil process objects 49 | self.proc_bp = None 50 | self.proc_awg = None 51 | self.proc_hwg = None 52 | 53 | atexit.register(self.killall) 54 | self.skip_backplane = False 55 | 56 | # start backplane 57 | self.proc_bp = self.start_backplane() 58 | if self.proc_bp: 59 | print('backplane started') 60 | 61 | else: 62 | print('backplane start failed - exiting') 63 | sys.exit(0) 64 | 65 | self.proc_awg = self.start_wsgw() 66 | if self.proc_awg: 67 | print('Websocket Gateway started') 68 | else: 69 | print('WebSocket Gateway start failed - exiting') 70 | sys.exit(0) 71 | 72 | # start arduino gateway 73 | self.proc_hwg = self.start_ardgw() 74 | if self.proc_hwg: 75 | print('Arduino Gateway started.') 76 | seconds = 5 77 | while seconds >= 0: 78 | print('\rPlease wait ' + str( 79 | seconds) + ' seconds for Arduino to initialize...', end='') 80 | time.sleep(1) 81 | seconds -= 1 82 | print() 83 | print('Arduino is initialized.') 84 | print('To exit this program, press Control-c') 85 | else: 86 | print('Arduino Gateway start failed - exiting') 87 | sys.exit(0) 88 | 89 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 90 | 91 | while True: 92 | try: 93 | if not self.skip_backplane: 94 | if self.proc_bp.poll() is not None: 95 | self.proc_bp = None 96 | print('backplane exited...') 97 | self.killall() 98 | if self.proc_awg.poll() is not None: 99 | self.proc_awg = None 100 | print('Websocket Gateway exited...') 101 | self.killall() 102 | if self.proc_hwg.poll() is not None: 103 | self.proc_hwg = None 104 | print('Arduino Gateway exited. Is your Arduino plugged in?') 105 | self.killall() 106 | 107 | # allow some time between polls 108 | time.sleep(.4) 109 | except Exception as e: 110 | sys.exit(0) 111 | 112 | def killall(self): 113 | """ 114 | Kill all running processes 115 | """ 116 | # prevent loop from running for a clean exit 117 | # check for missing processes 118 | if self.proc_bp: 119 | try: 120 | if sys.platform.startswith('win32'): 121 | subprocess.run( 122 | ['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 123 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 124 | subprocess.CREATE_NO_WINDOW 125 | ) 126 | else: 127 | self.proc_bp.kill() 128 | self.proc_bp = None 129 | except: 130 | pass 131 | if self.proc_awg: 132 | try: 133 | if sys.platform.startswith('win32'): 134 | subprocess.run( 135 | ['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 136 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 137 | subprocess.CREATE_NO_WINDOW 138 | ) 139 | else: 140 | self.proc_awg.kill() 141 | self.proc_awg = None 142 | except: 143 | pass 144 | if self.proc_hwg: 145 | try: 146 | if sys.platform.startswith('win32'): 147 | subprocess.run( 148 | ['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 149 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 150 | subprocess.CREATE_NO_WINDOW 151 | ) 152 | else: 153 | self.proc_hwg.kill() 154 | self.proc_hwg = None 155 | except: 156 | pass 157 | # sys.exit(0) 158 | 159 | def start_backplane(self): 160 | """ 161 | Start the backplane 162 | """ 163 | 164 | # check to see if the backplane is already running 165 | try: 166 | for proc in psutil.process_iter(attrs=['pid', 'name']): 167 | if 'backplane' in proc.info['name']: 168 | self.skip_backplane = True 169 | # its running - return its pid 170 | return proc 171 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 172 | pass 173 | 174 | # backplane is not running, so start one 175 | if sys.platform.startswith('win32'): 176 | return subprocess.Popen(['backplane'], 177 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 178 | subprocess.CREATE_NO_WINDOW) 179 | else: 180 | return subprocess.Popen(['backplane'], 181 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 182 | stdout=subprocess.PIPE) 183 | 184 | def start_wsgw(self): 185 | """ 186 | Start the websocket gateway 187 | """ 188 | if sys.platform.startswith('win32'): 189 | return subprocess.Popen(['wsgw'], 190 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 191 | | 192 | subprocess.CREATE_NO_WINDOW) 193 | else: 194 | return subprocess.Popen(['wsgw'], 195 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 196 | stdout=subprocess.PIPE) 197 | 198 | def start_ardgw(self): 199 | """ 200 | Start the arduino gateway 201 | """ 202 | if sys.platform.startswith('win32'): 203 | hwgw_start = ['ardgw'] 204 | else: 205 | hwgw_start = ['ardgw'] 206 | 207 | if self.com_port: 208 | hwgw_start.append('-c') 209 | hwgw_start.append(self.com_port) 210 | 211 | if sys.platform.startswith('win32'): 212 | return subprocess.Popen(hwgw_start, 213 | creationflags=subprocess.CREATE_NO_WINDOW) 214 | else: 215 | return subprocess.Popen(hwgw_start, 216 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 217 | stdout=subprocess.PIPE) 218 | 219 | 220 | def signal_handler(sig, frame): 221 | print('Exiting Through Signal Handler') 222 | # raise KeyboardInterrupt 223 | 224 | 225 | def s3ax(): 226 | parser = argparse.ArgumentParser() 227 | parser.add_argument("-c", dest="com_port", default="None", 228 | help="Use this COM port instead of auto discovery") 229 | parser.add_argument("-i", dest="arduino_instance_id", default="None", 230 | help="Set an Arduino Instance ID and match it in FirmataExpress") 231 | args = parser.parse_args() 232 | 233 | if args.com_port == "None": 234 | com_port = None 235 | else: 236 | com_port = args.com_port 237 | 238 | if args.arduino_instance_id == "None": 239 | arduino_instance_id = None 240 | else: 241 | arduino_instance_id = int(args.arduino_instance_id) 242 | 243 | if com_port and arduino_instance_id: 244 | raise RuntimeError( 245 | 'Both com_port arduino_instance_id were set. Only one is allowed') 246 | 247 | S3A(com_port=com_port, arduino_instance_id=args.arduino_instance_id) 248 | 249 | 250 | # listen for SIGINT 251 | signal.signal(signal.SIGINT, signal_handler) 252 | signal.signal(signal.SIGTERM, signal_handler) 253 | 254 | if __name__ == '__main__': 255 | # replace with name of function you defined above 256 | s3ax() 257 | -------------------------------------------------------------------------------- /s3_extend/s3c.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2020 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | import atexit 18 | import signal 19 | import subprocess 20 | import sys 21 | import time 22 | 23 | import psutil 24 | 25 | 26 | # import webbrowser 27 | 28 | 29 | class S3C: 30 | """ 31 | This class starts the Banyan server to support the Scratch 3 OneGPIO Playground Express 32 | extension. 33 | 34 | It will start the backplane, Playground express gateway and websocket gateway. 35 | """ 36 | 37 | def __init__(self): 38 | """ 39 | Prepare for launching the cpx extension 40 | """ 41 | 42 | self.proc_bp = None 43 | self.proc_awg = None 44 | self.proc_hwg = None 45 | 46 | self.skip_backplane = False 47 | 48 | # start backplane 49 | self.proc_bp = self.start_backplane() 50 | if self.proc_bp: 51 | print('backplane started') 52 | 53 | else: 54 | print('backplane start failed - exiting') 55 | sys.exit(0) 56 | 57 | self.proc_awg = self.start_wsgw() 58 | if self.proc_awg: 59 | print('Websocket Gateway started') 60 | else: 61 | print('WebSocket Gateway start failed - exiting') 62 | sys.exit(0) 63 | 64 | # start cpx gateway 65 | self.proc_hwg = self.start_cpxgw() 66 | if self.proc_hwg: 67 | print('Playground Express Gateway started ') 68 | print('To exit this program, press Control-c') 69 | 70 | else: 71 | print('Playground Express Gateway start failed - exiting') 72 | sys.exit(0) 73 | 74 | atexit.register(self.killall) 75 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 76 | 77 | while True: 78 | try: 79 | if not self.skip_backplane: 80 | if self.proc_bp.poll() is not None: 81 | self.proc_bp = None 82 | print('backplane exited...') 83 | self.killall() 84 | if self.proc_awg.poll() is not None: 85 | self.proc_awg = None 86 | print('Websocket Gateway exited...') 87 | self.killall() 88 | if self.proc_hwg.poll() is not None: 89 | self.proc_hwg = None 90 | print('CPX Gateway exited.') 91 | self.killall() 92 | 93 | # allow some time between polls 94 | time.sleep(.4) 95 | except Exception as e: 96 | sys.exit(0) 97 | 98 | def killall(self): 99 | """ 100 | Kill all running processes 101 | """ 102 | 103 | # check for missing processes 104 | if self.proc_bp: 105 | try: 106 | if sys.platform.startswith('win32'): 107 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 108 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 109 | subprocess.CREATE_NO_WINDOW 110 | ) 111 | else: 112 | self.proc_bp.kill() 113 | self.proc_bp = None 114 | except: 115 | pass 116 | if self.proc_awg: 117 | try: 118 | if sys.platform.startswith('win32'): 119 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 120 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 121 | subprocess.CREATE_NO_WINDOW 122 | ) 123 | else: 124 | self.proc_awg.kill() 125 | self.proc_awg = None 126 | except: 127 | pass 128 | if self.proc_hwg: 129 | try: 130 | if sys.platform.startswith('win32'): 131 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 132 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 133 | subprocess.CREATE_NO_WINDOW 134 | ) 135 | else: 136 | self.proc_hwg.kill() 137 | self.proc_hwg = None 138 | except: 139 | pass 140 | # sys.exit(0) 141 | 142 | def start_backplane(self): 143 | """ 144 | Start the backplane 145 | """ 146 | 147 | # check to see if the backplane is already running 148 | try: 149 | for proc in psutil.process_iter(attrs=['pid', 'name']): 150 | if 'backplane' in proc.info['name']: 151 | self.skip_backplane = True 152 | # its running - return its pid 153 | return proc 154 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 155 | pass 156 | 157 | # backplane is not running, so start one 158 | if sys.platform.startswith('win32'): 159 | return subprocess.Popen(['backplane'], 160 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 161 | subprocess.CREATE_NO_WINDOW) 162 | else: 163 | return subprocess.Popen(['backplane'], 164 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 165 | stdout=subprocess.PIPE) 166 | 167 | def start_wsgw(self): 168 | """ 169 | Start the websocket gateway 170 | """ 171 | if sys.platform.startswith('win32'): 172 | return subprocess.Popen(['wsgw', '-i', '9003'], 173 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 174 | | 175 | subprocess.CREATE_NO_WINDOW) 176 | else: 177 | return subprocess.Popen(['wsgw', '-i', '9003'], 178 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 179 | stdout=subprocess.PIPE) 180 | 181 | def start_cpxgw(self): 182 | """ 183 | Start the rpi gateway 184 | """ 185 | if sys.platform.startswith('win32'): 186 | return subprocess.Popen(['cpxgw'], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 187 | subprocess.CREATE_NO_WINDOW) 188 | else: 189 | return subprocess.Popen(['cpxgw'], stdin=subprocess.PIPE, stderr=subprocess.PIPE, 190 | stdout=subprocess.PIPE) 191 | 192 | 193 | def signal_handler(sig, frame): 194 | print('Exiting Through Signal Handler') 195 | # raise KeyboardInterrupt 196 | 197 | 198 | def s3cx(): 199 | """ 200 | Start the extension 201 | :return: 202 | """ 203 | S3C() 204 | 205 | 206 | # listen for SIGINT 207 | signal.signal(signal.SIGINT, signal_handler) 208 | signal.signal(signal.SIGTERM, signal_handler) 209 | 210 | if __name__ == '__main__': 211 | # replace with name of function you defined above 212 | s3cx() 213 | -------------------------------------------------------------------------------- /s3_extend/s3e.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | 18 | import atexit 19 | import psutil 20 | import signal 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | # import webbrowser 26 | 27 | 28 | class S3E: 29 | """ 30 | This class starts the Banyan server to support the Scratch 3 OneGPIO ESP-8266 31 | extension. 32 | 33 | It will start the backplane, ESP-8266 gateway and websocket gateway. 34 | The wait for IP address flag (-w) is set. 35 | """ 36 | 37 | def __init__(self): 38 | """ 39 | Launch the esp extension 40 | """ 41 | 42 | self.proc_bp = None 43 | self.proc_awg = None 44 | self.proc_hwg = None 45 | 46 | self.skip_backplane = False 47 | 48 | # start backplane 49 | self.proc_bp = self.start_backplane() 50 | if self.proc_bp: 51 | print('backplane started') 52 | 53 | else: 54 | print('backplane start failed - exiting') 55 | sys.exit(0) 56 | 57 | self.proc_awg = self.start_wsgw() 58 | if self.proc_awg: 59 | print('Websocket Gateway started') 60 | else: 61 | print('WebSocket Gateway start failed - exiting') 62 | sys.exit(0) 63 | 64 | # start esp gateway 65 | self.proc_hwg = self.start_espgw() 66 | if self.proc_hwg: 67 | print('ESP-8266 Gateway started ') 68 | print('To exit this program, press Control-c') 69 | 70 | else: 71 | print('ESP-8266 Gateway start failed - exiting') 72 | sys.exit(0) 73 | 74 | atexit.register(self.killall) 75 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 76 | 77 | while True: 78 | try: 79 | if not self.skip_backplane: 80 | if self.proc_bp.poll() is not None: 81 | self.proc_bp = None 82 | print('backplane exited...') 83 | self.killall() 84 | if self.proc_awg.poll() is not None: 85 | self.proc_awg = None 86 | print('Websocket Gateway exited...') 87 | self.killall() 88 | if self.proc_hwg.poll() is not None: 89 | self.proc_hwg = None 90 | print('ESP-8266 Gateway exited.') 91 | self.killall() 92 | 93 | # allow some time between polls 94 | time.sleep(.4) 95 | except Exception as e: 96 | sys.exit(0) 97 | 98 | def killall(self): 99 | """ 100 | Kill all running processes 101 | """ 102 | 103 | # check for missing processes 104 | if self.proc_bp: 105 | try: 106 | if sys.platform.startswith('win32'): 107 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 108 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 109 | subprocess.CREATE_NO_WINDOW 110 | ) 111 | else: 112 | self.proc_bp.kill() 113 | self.proc_bp = None 114 | except: 115 | pass 116 | if self.proc_awg: 117 | try: 118 | if sys.platform.startswith('win32'): 119 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 120 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 121 | subprocess.CREATE_NO_WINDOW 122 | ) 123 | else: 124 | self.proc_awg.kill() 125 | self.proc_awg = None 126 | except: 127 | pass 128 | if self.proc_hwg: 129 | try: 130 | if sys.platform.startswith('win32'): 131 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 132 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 133 | subprocess.CREATE_NO_WINDOW 134 | ) 135 | else: 136 | self.proc_hwg.kill() 137 | self.proc_hwg = None 138 | except: 139 | pass 140 | # sys.exit(0) 141 | 142 | def start_backplane(self): 143 | """ 144 | Start the backplane 145 | """ 146 | 147 | 148 | # check to see if the backplane is already running 149 | try: 150 | for proc in psutil.process_iter(attrs=['pid', 'name']): 151 | if 'backplane' in proc.info['name']: 152 | self.skip_backplane = True 153 | # its running - return its pid 154 | return proc 155 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 156 | pass 157 | 158 | # backplane is not running, so start one 159 | if sys.platform.startswith('win32'): 160 | return subprocess.Popen(['backplane'], 161 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 162 | subprocess.CREATE_NO_WINDOW) 163 | else: 164 | return subprocess.Popen(['backplane'], 165 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 166 | stdout=subprocess.PIPE) 167 | 168 | def start_wsgw(self): 169 | """ 170 | Start the websocket gateway 171 | """ 172 | if sys.platform.startswith('win32'): 173 | return subprocess.Popen(['wsgw', '-i', '9002'], 174 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 175 | | 176 | subprocess.CREATE_NO_WINDOW) 177 | else: 178 | return subprocess.Popen(['wsgw', '-i', '9002'], 179 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 180 | stdout=subprocess.PIPE) 181 | 182 | def start_espgw(self): 183 | """ 184 | Start the esp_8266 gateway 185 | """ 186 | if sys.platform.startswith('win32'): 187 | return subprocess.Popen(['espgw' ], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 188 | subprocess.CREATE_NO_WINDOW) 189 | else: 190 | return subprocess.Popen(['espgw' ], stdin=subprocess.PIPE, stderr=subprocess.PIPE, 191 | stdout=subprocess.PIPE) 192 | 193 | 194 | def signal_handler(sig, frame): 195 | print('Exiting Through Signal Handler') 196 | # raise KeyboardInterrupt 197 | 198 | 199 | def s3ex(): 200 | 201 | # instantiate 202 | S3E() 203 | 204 | 205 | # listen for SIGINT 206 | signal.signal(signal.SIGINT, signal_handler) 207 | signal.signal(signal.SIGTERM, signal_handler) 208 | 209 | if __name__ == '__main__': 210 | # replace with name of function you defined above 211 | s3ex() 212 | -------------------------------------------------------------------------------- /s3_extend/s3p.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | 15 | along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | """ 18 | import argparse 19 | import atexit 20 | import signal 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | import psutil 26 | 27 | 28 | # import webbrowser 29 | 30 | 31 | class S3P: 32 | """ 33 | This class starts the Banyan server to support Scratch 3 OneGPIO 34 | for the Picoboard 35 | 36 | It will start the backplane, picoboard gateway and websocket gateway. 37 | """ 38 | 39 | def __init__(self, com_port=None): 40 | """ 41 | :param com_port: Manually select serial com port 42 | """ 43 | 44 | self.com_port = com_port 45 | 46 | # psutil pids 47 | self.proc_bp = None 48 | self.proc_awg = None 49 | self.proc_hwg = None 50 | 51 | atexit.register(self.killall) 52 | self.skip_backplane = False 53 | 54 | # start backplane 55 | self.proc_bp = self.start_backplane() 56 | if self.proc_bp: 57 | print('backplane started') 58 | 59 | else: 60 | print('backplane start failed - exiting') 61 | sys.exit(0) 62 | 63 | self.proc_awg = self.start_wsgw() 64 | if self.proc_awg: 65 | print('Websocket Gateway started') 66 | else: 67 | print('WebSocket Gateway start failed - exiting') 68 | sys.exit(0) 69 | 70 | # start picoboard gateway 71 | self.proc_hwg = self.start_pbgw() 72 | if self.proc_hwg: 73 | print('Picoboard Gateway started ') 74 | print('To exit this program, press Control-c') 75 | 76 | else: 77 | print('Picobard Gateway start failed - exiting') 78 | sys.exit(0) 79 | 80 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 81 | 82 | while True: 83 | try: 84 | if not self.skip_backplane: 85 | if self.proc_bp.poll() is not None: 86 | self.proc_bp = None 87 | print('backplane exited...') 88 | self.killall() 89 | if self.proc_awg.poll() is not None: 90 | self.proc_awg = None 91 | print('Websocket Gateway exited...') 92 | self.killall() 93 | if self.proc_hwg.poll() is not None: 94 | self.proc_hwg = None 95 | print('Picoboard Gateway exited. Is your Picoboard plugged in?') 96 | self.killall() 97 | 98 | # allow some time between polls 99 | time.sleep(.4) 100 | except Exception as e: 101 | sys.exit(0) 102 | 103 | def killall(self): 104 | """ 105 | Kill all running processes 106 | """ 107 | # prevent loop from running for a clean exit 108 | # self.stop_event.set() 109 | # check for missing processes 110 | if self.proc_bp: 111 | try: 112 | if sys.platform.startswith('win32'): 113 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 114 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 115 | subprocess.CREATE_NO_WINDOW 116 | ) 117 | else: 118 | self.proc_bp.kill() 119 | self.proc_bp = None 120 | except: 121 | pass 122 | if self.proc_awg: 123 | try: 124 | if sys.platform.startswith('win32'): 125 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 126 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 127 | subprocess.CREATE_NO_WINDOW 128 | ) 129 | else: 130 | self.proc_awg.kill() 131 | self.proc_awg = None 132 | except: 133 | pass 134 | if self.proc_hwg: 135 | try: 136 | if sys.platform.startswith('win32'): 137 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 138 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 139 | subprocess.CREATE_NO_WINDOW 140 | ) 141 | else: 142 | self.proc_hwg.kill() 143 | self.proc_hwg = None 144 | except: 145 | pass 146 | # sys.exit(0) 147 | 148 | def start_backplane(self): 149 | """ 150 | Start the backplane 151 | """ 152 | 153 | # check to see if the backplane is already running 154 | try: 155 | for proc in psutil.process_iter(attrs=['pid', 'name']): 156 | if 'backplane' in proc.info['name']: 157 | self.skip_backplane = True 158 | # its running - return its pid 159 | return proc 160 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 161 | pass 162 | 163 | # backplane is not running, so start one 164 | if sys.platform.startswith('win32'): 165 | return subprocess.Popen(['backplane'], 166 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 167 | subprocess.CREATE_NO_WINDOW) 168 | else: 169 | return subprocess.Popen(['backplane'], 170 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 171 | stdout=subprocess.PIPE) 172 | 173 | def start_wsgw(self): 174 | """ 175 | Start the websocket gateway 176 | """ 177 | if sys.platform.startswith('win32'): 178 | return subprocess.Popen(['wsgw', '-i', '9004'], 179 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 180 | | 181 | subprocess.CREATE_NO_WINDOW) 182 | else: 183 | return subprocess.Popen(['wsgw', '-i', '9004'], 184 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 185 | stdout=subprocess.PIPE) 186 | 187 | def start_pbgw(self): 188 | """ 189 | Start the picoboard gateway 190 | """ 191 | if sys.platform.startswith('win32'): 192 | return subprocess.Popen(['pbgw'], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 193 | subprocess.CREATE_NO_WINDOW) 194 | else: 195 | return subprocess.Popen(['pbgw'], stdin=subprocess.PIPE, stderr=subprocess.PIPE, 196 | stdout=subprocess.PIPE) 197 | 198 | 199 | def signal_handler(sig, frame): 200 | print('Exiting Through Signal Handler') 201 | # raise KeyboardInterrupt 202 | 203 | 204 | def s3px(): 205 | """ 206 | Start the s3p script 207 | """ 208 | parser = argparse.ArgumentParser() 209 | parser.add_argument("-c", dest="com_port", default="None", 210 | help="Use this COM port instead of auto discovery") 211 | 212 | args = parser.parse_args() 213 | 214 | if args.com_port == "None": 215 | com_port = None 216 | else: 217 | com_port = args.com_port 218 | 219 | S3P(com_port=com_port) 220 | 221 | 222 | # listen for SIGINT 223 | signal.signal(signal.SIGINT, signal_handler) 224 | signal.signal(signal.SIGTERM, signal_handler) 225 | 226 | if __name__ == '__main__': 227 | # replace with name of function you defined above 228 | s3px() 229 | -------------------------------------------------------------------------------- /s3_extend/s3r.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | import atexit 18 | import signal 19 | import subprocess 20 | import sys 21 | import time 22 | 23 | import psutil 24 | 25 | 26 | # import webbrowser 27 | 28 | 29 | class S3R: 30 | """ 31 | This class starts the Banyan server to support the Scratch 3 OneGPIO Raspberry Pi 32 | extension for local mode. That means the browser and components are all executing 33 | on a single RPi. 34 | 35 | It will start the backplane, Raspberry Pi gateway and websocket gateway. 36 | """ 37 | 38 | def __init__(self): 39 | """ 40 | Prepare for launching the rpi extension 41 | """ 42 | 43 | self.proc_bp = None 44 | self.proc_awg = None 45 | self.proc_hwg = None 46 | 47 | self.skip_backplane = False 48 | 49 | print("Only run this script on a Raspberry Pi!") 50 | 51 | # start backplane 52 | self.proc_bp = self.start_backplane() 53 | if self.proc_bp: 54 | print('backplane started') 55 | 56 | else: 57 | print('backplane start failed - exiting') 58 | sys.exit(0) 59 | 60 | self.proc_awg = self.start_wsgw() 61 | if self.proc_awg: 62 | print('Websocket Gateway started') 63 | else: 64 | print('WebSocket Gateway start failed - exiting') 65 | sys.exit(0) 66 | 67 | # start rpi gateway 68 | self.proc_hwg = self.start_rpigw() 69 | if self.proc_hwg: 70 | print('RPi Gateway started ') 71 | print('To exit this program, press Control-c') 72 | 73 | else: 74 | print('RPi Gateway start failed - exiting') 75 | sys.exit(0) 76 | 77 | atexit.register(self.killall) 78 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 79 | 80 | while True: 81 | try: 82 | if not self.skip_backplane: 83 | if self.proc_bp.poll() is not None: 84 | self.proc_bp = None 85 | print('backplane exited...') 86 | self.killall() 87 | if self.proc_awg.poll() is not None: 88 | self.proc_awg = None 89 | print('Websocket Gateway exited...') 90 | self.killall() 91 | if self.proc_hwg.poll() is not None: 92 | self.proc_hwg = None 93 | print('RPi Gateway exited.') 94 | self.killall() 95 | 96 | # allow some time between polls 97 | time.sleep(.4) 98 | except Exception as e: 99 | sys.exit(0) 100 | 101 | def killall(self): 102 | """ 103 | Kill all running processes 104 | """ 105 | 106 | # check for missing processes 107 | if self.proc_bp: 108 | try: 109 | if sys.platform.startswith('win32'): 110 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 111 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 112 | subprocess.CREATE_NO_WINDOW 113 | ) 114 | else: 115 | self.proc_bp.kill() 116 | self.proc_bp = None 117 | except: 118 | pass 119 | if self.proc_awg: 120 | try: 121 | if sys.platform.startswith('win32'): 122 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 123 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 124 | subprocess.CREATE_NO_WINDOW 125 | ) 126 | else: 127 | self.proc_awg.kill() 128 | self.proc_awg = None 129 | except: 130 | pass 131 | if self.proc_hwg: 132 | try: 133 | if sys.platform.startswith('win32'): 134 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 135 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 136 | subprocess.CREATE_NO_WINDOW 137 | ) 138 | else: 139 | self.proc_hwg.kill() 140 | self.proc_hwg = None 141 | except: 142 | pass 143 | # sys.exit(0) 144 | 145 | def start_backplane(self): 146 | """ 147 | Start the backplane 148 | """ 149 | 150 | # check to see if the backplane is already running 151 | try: 152 | for proc in psutil.process_iter(attrs=['pid', 'name']): 153 | if 'backplane' in proc.info['name']: 154 | self.skip_backplane = True 155 | # its running - return its pid 156 | return proc 157 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 158 | pass 159 | 160 | # backplane is not running, so start one 161 | if sys.platform.startswith('win32'): 162 | return subprocess.Popen(['backplane'], 163 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 164 | subprocess.CREATE_NO_WINDOW) 165 | else: 166 | return subprocess.Popen(['backplane'], 167 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 168 | stdout=subprocess.PIPE) 169 | 170 | def start_wsgw(self): 171 | """ 172 | Start the websocket gateway 173 | """ 174 | if sys.platform.startswith('win32'): 175 | return subprocess.Popen(['wsgw', '-i', '9001'], 176 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 177 | | 178 | subprocess.CREATE_NO_WINDOW) 179 | else: 180 | return subprocess.Popen(['wsgw', '-i', '9001'], 181 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 182 | stdout=subprocess.PIPE) 183 | 184 | def start_rpigw(self): 185 | """ 186 | Start the rpi gateway 187 | """ 188 | if sys.platform.startswith('win32'): 189 | return subprocess.Popen(['rpigw'], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 190 | subprocess.CREATE_NO_WINDOW) 191 | else: 192 | return subprocess.Popen(['rpigw'], stdin=subprocess.PIPE, stderr=subprocess.PIPE, 193 | stdout=subprocess.PIPE) 194 | 195 | 196 | def signal_handler(sig, frame): 197 | print('Exiting Through Signal Handler') 198 | # raise KeyboardInterrupt 199 | 200 | 201 | def s3rx(): 202 | """ 203 | Start the extension 204 | :return: 205 | """ 206 | S3R() 207 | 208 | 209 | # listen for SIGINT 210 | signal.signal(signal.SIGINT, signal_handler) 211 | signal.signal(signal.SIGTERM, signal_handler) 212 | 213 | if __name__ == '__main__': 214 | # replace with name of function you defined above 215 | s3rx() 216 | -------------------------------------------------------------------------------- /s3_extend/s3rh.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | 15 | along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | """ 18 | import argparse 19 | import atexit 20 | import signal 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | import psutil 26 | 27 | 28 | # import webbrowser 29 | 30 | 31 | class S3RH: 32 | """ 33 | This class starts the Banyan server to support Scratch 3 OneGPIO 34 | for the RoboHAT MM1 35 | 36 | It will start the backplane, robohat gateway and websocket gateway. 37 | """ 38 | 39 | def __init__(self, com_port=None, arduino_instance_id=None): 40 | """ 41 | 42 | :param com_port: 43 | :param arduino_instance_id: 44 | """ 45 | 46 | self.com_port = com_port 47 | 48 | # psutil process objects 49 | self.proc_bp = None 50 | self.proc_awg = None 51 | self.proc_hwg = None 52 | 53 | atexit.register(self.killall) 54 | self.skip_backplane = False 55 | 56 | # start backplane 57 | self.proc_bp = self.start_backplane() 58 | if self.proc_bp: 59 | print('backplane started') 60 | 61 | else: 62 | print('backplane start failed - exiting') 63 | sys.exit(0) 64 | 65 | self.proc_awg = self.start_wsgw() 66 | if self.proc_awg: 67 | print('Websocket Gateway started') 68 | else: 69 | print('WebSocket Gateway start failed - exiting') 70 | sys.exit(0) 71 | 72 | # start robohat gateway 73 | self.proc_hwg = self.start_rhgw() 74 | if self.proc_hwg: 75 | print('Robohat Gateway started.') 76 | seconds = 5 77 | while seconds >= 0: 78 | print('\rPlease wait ' + str(seconds) + ' seconds for Robohat to initialize...', end='') 79 | time.sleep(1) 80 | seconds -= 1 81 | print() 82 | print('Robohat is initialized.') 83 | print('To exit this program, press Control-c') 84 | else: 85 | print('RoboHAT Gateway start failed - exiting') 86 | sys.exit(0) 87 | 88 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 89 | 90 | while True: 91 | try: 92 | if not self.skip_backplane: 93 | if self.proc_bp.poll() is not None: 94 | self.proc_bp = None 95 | print('backplane exited...') 96 | self.killall() 97 | if self.proc_awg.poll() is not None: 98 | self.proc_awg = None 99 | print('Websocket Gateway exited...') 100 | self.killall() 101 | if self.proc_hwg.poll() is not None: 102 | self.proc_hwg = None 103 | print('RoboHAT Gateway exited. Is your RoboHAT plugged in?') 104 | self.killall() 105 | 106 | # allow some time between polls 107 | time.sleep(.4) 108 | except Exception as e: 109 | sys.exit(0) 110 | 111 | def killall(self): 112 | """ 113 | Kill all running processes 114 | """ 115 | # prevent loop from running for a clean exit 116 | # check for missing processes 117 | if self.proc_bp: 118 | try: 119 | if sys.platform.startswith('win32'): 120 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 121 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 122 | subprocess.CREATE_NO_WINDOW 123 | ) 124 | else: 125 | self.proc_bp.kill() 126 | self.proc_bp = None 127 | except: 128 | pass 129 | if self.proc_awg: 130 | try: 131 | if sys.platform.startswith('win32'): 132 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 133 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 134 | subprocess.CREATE_NO_WINDOW 135 | ) 136 | else: 137 | self.proc_awg.kill() 138 | self.proc_awg = None 139 | except: 140 | pass 141 | if self.proc_hwg: 142 | try: 143 | if sys.platform.startswith('win32'): 144 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 145 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 146 | subprocess.CREATE_NO_WINDOW 147 | ) 148 | else: 149 | self.proc_hwg.kill() 150 | self.proc_hwg = None 151 | except: 152 | pass 153 | # sys.exit(0) 154 | 155 | def start_backplane(self): 156 | """ 157 | Start the backplane 158 | """ 159 | 160 | # check to see if the backplane is already running 161 | try: 162 | for proc in psutil.process_iter(attrs=['pid', 'name']): 163 | if 'backplane' in proc.info['name']: 164 | self.skip_backplane = True 165 | # its running - return its pid 166 | return proc 167 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 168 | pass 169 | 170 | # backplane is not running, so start one 171 | if sys.platform.startswith('win32'): 172 | return subprocess.Popen(['backplane'], 173 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 174 | subprocess.CREATE_NO_WINDOW) 175 | else: 176 | return subprocess.Popen(['backplane'], 177 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 178 | stdout=subprocess.PIPE) 179 | 180 | def start_wsgw(self): 181 | """ 182 | Start the websocket gateway 183 | """ 184 | if sys.platform.startswith('win32'): 185 | return subprocess.Popen(['wsgw', '-i', '9005'], 186 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 187 | | 188 | subprocess.CREATE_NO_WINDOW) 189 | else: 190 | return subprocess.Popen(['wsgw', '-i', '9005'], 191 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 192 | stdout=subprocess.PIPE) 193 | 194 | def start_rhgw(self): 195 | """ 196 | Start the robohat gateway 197 | """ 198 | if sys.platform.startswith('win32'): 199 | hwgw_start = ['rhgw'] 200 | else: 201 | hwgw_start = ['rhgw'] 202 | 203 | if self.com_port: 204 | hwgw_start.append('-c') 205 | hwgw_start.append(self.com_port) 206 | 207 | if sys.platform.startswith('win32'): 208 | return subprocess.Popen(hwgw_start, 209 | creationflags=subprocess.CREATE_NO_WINDOW) 210 | else: 211 | return subprocess.Popen(hwgw_start, 212 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 213 | stdout=subprocess.PIPE) 214 | 215 | 216 | def signal_handler(sig, frame): 217 | print('Exiting Through Signal Handler') 218 | # raise KeyboardInterrupt 219 | 220 | 221 | def s3rhx(): 222 | parser = argparse.ArgumentParser() 223 | parser.add_argument("-c", dest="com_port", default="None", 224 | help="Use this COM port instead of auto discovery") 225 | parser.add_argument("-i", dest="arduino_instance_id", default="None", 226 | help="Set an Arduino Instance ID and match it in FirmataExpress") 227 | args = parser.parse_args() 228 | 229 | if args.com_port == "None": 230 | com_port = None 231 | else: 232 | com_port = args.com_port 233 | 234 | if args.arduino_instance_id == "None": 235 | arduino_instance_id = None 236 | else: 237 | arduino_instance_id = int(args.arduino_instance_id) 238 | 239 | if com_port and arduino_instance_id: 240 | raise RuntimeError('Both com_port arduino_instance_id were set. Only one is allowed') 241 | 242 | S3RH(com_port=com_port, arduino_instance_id=args.arduino_instance_id) 243 | 244 | 245 | # listen for SIGINT 246 | signal.signal(signal.SIGINT, signal_handler) 247 | signal.signal(signal.SIGTERM, signal_handler) 248 | 249 | if __name__ == '__main__': 250 | # replace with name of function you defined above 251 | s3rhx() 252 | -------------------------------------------------------------------------------- /s3_extend/s3rp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2019 Alan Yorinks All rights reserved. 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3 as published by the Free Software Foundation; either 7 | or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE 14 | along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | """ 17 | import atexit 18 | import signal 19 | import subprocess 20 | import sys 21 | import time 22 | 23 | import psutil 24 | 25 | 26 | # import webbrowser 27 | 28 | 29 | class S3RP: 30 | """ 31 | This class starts the Banyan server to support the Scratch 3 OneGPIO Raspberry Pi 32 | extension for local mode. That means the browser and components are all executing 33 | on a single RPi. 34 | 35 | It will start the backplane, Raspberry Pi gateway and websocket gateway. 36 | """ 37 | 38 | def __init__(self): 39 | """ 40 | Prepare for launching the rpi extension 41 | """ 42 | 43 | self.proc_bp = None 44 | self.proc_awg = None 45 | self.proc_hwg = None 46 | 47 | self.skip_backplane = False 48 | 49 | # start backplane 50 | self.proc_bp = self.start_backplane() 51 | if self.proc_bp: 52 | print('backplane started') 53 | 54 | else: 55 | print('backplane start failed - exiting') 56 | sys.exit(0) 57 | 58 | self.proc_awg = self.start_wsgw() 59 | if self.proc_awg: 60 | print('Websocket Gateway started') 61 | else: 62 | print('WebSocket Gateway start failed - exiting') 63 | sys.exit(0) 64 | 65 | # start rpi gateway 66 | self.proc_hwg = self.start_rp_gw() 67 | if self.proc_hwg: 68 | print('RPi Pico Gateway started ') 69 | print('To exit this program, press Control-c') 70 | 71 | else: 72 | print('RPi Gateway start failed - exiting') 73 | sys.exit(0) 74 | 75 | atexit.register(self.killall) 76 | # webbrowser.open('https://mryslab.github.io/s3onegpio/', new=1) 77 | 78 | while True: 79 | try: 80 | if not self.skip_backplane: 81 | if self.proc_bp.poll() is not None: 82 | self.proc_bp = None 83 | print('backplane exited...') 84 | self.killall() 85 | if self.proc_awg.poll() is not None: 86 | self.proc_awg = None 87 | print('Websocket Gateway exited...') 88 | self.killall() 89 | if self.proc_hwg.poll() is not None: 90 | self.proc_hwg = None 91 | print('RPi Pico Gateway exited.') 92 | self.killall() 93 | 94 | # allow some time between polls 95 | time.sleep(.4) 96 | except Exception as e: 97 | sys.exit(0) 98 | 99 | def killall(self): 100 | """ 101 | Kill all running processes 102 | """ 103 | 104 | # check for missing processes 105 | if self.proc_bp: 106 | try: 107 | if sys.platform.startswith('win32'): 108 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_bp.pid)], 109 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 110 | subprocess.CREATE_NO_WINDOW 111 | ) 112 | else: 113 | self.proc_bp.kill() 114 | self.proc_bp = None 115 | except: 116 | pass 117 | if self.proc_awg: 118 | try: 119 | if sys.platform.startswith('win32'): 120 | subprocess.run(['taskkill', '/F', '/t', '/pid', str(self.proc_awg.pid)], 121 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 122 | subprocess.CREATE_NO_WINDOW 123 | ) 124 | else: 125 | self.proc_awg.kill() 126 | self.proc_awg = None 127 | except: 128 | pass 129 | if self.proc_hwg: 130 | try: 131 | if sys.platform.startswith('win32'): 132 | subprocess.run(['taskkill', '/F', '/t', '/PID', str(self.proc_hwg.pid)], 133 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 134 | subprocess.CREATE_NO_WINDOW 135 | ) 136 | else: 137 | self.proc_hwg.kill() 138 | self.proc_hwg = None 139 | except: 140 | pass 141 | # sys.exit(0) 142 | 143 | def start_backplane(self): 144 | """ 145 | Start the backplane 146 | """ 147 | 148 | # check to see if the backplane is already running 149 | try: 150 | for proc in psutil.process_iter(attrs=['pid', 'name']): 151 | if 'backplane' in proc.info['name']: 152 | self.skip_backplane = True 153 | # its running - return its pid 154 | return proc 155 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 156 | pass 157 | 158 | # backplane is not running, so start one 159 | if sys.platform.startswith('win32'): 160 | return subprocess.Popen(['backplane'], 161 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 162 | subprocess.CREATE_NO_WINDOW) 163 | else: 164 | return subprocess.Popen(['backplane'], 165 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 166 | stdout=subprocess.PIPE) 167 | 168 | def start_wsgw(self): 169 | """ 170 | Start the websocket gateway 171 | """ 172 | if sys.platform.startswith('win32'): 173 | return subprocess.Popen(['wsgw', '-i', '9006'], 174 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 175 | | 176 | subprocess.CREATE_NO_WINDOW) 177 | else: 178 | return subprocess.Popen(['wsgw', '-i', '9006'], 179 | stdin=subprocess.PIPE, stderr=subprocess.PIPE, 180 | stdout=subprocess.PIPE) 181 | 182 | def start_rp_gw(self): 183 | """ 184 | Start the rpi pico gateway 185 | """ 186 | if sys.platform.startswith('win32'): 187 | return subprocess.Popen(['rpgw'], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | 188 | subprocess.CREATE_NO_WINDOW) 189 | else: 190 | return subprocess.Popen(['rpgw'], stdin=subprocess.PIPE, stderr=subprocess.PIPE, 191 | stdout=subprocess.PIPE) 192 | 193 | 194 | def signal_handler(sig, frame): 195 | print('Exiting Through Signal Handler') 196 | # raise KeyboardInterrupt 197 | 198 | 199 | def s3rpx(): 200 | """ 201 | Start the extension 202 | :return: 203 | """ 204 | S3RP() 205 | 206 | 207 | # listen for SIGINT 208 | signal.signal(signal.SIGINT, signal_handler) 209 | signal.signal(signal.SIGTERM, signal_handler) 210 | 211 | if __name__ == '__main__': 212 | # replace with name of function you defined above 213 | s3rpx() 214 | --------------------------------------------------------------------------------