Extended and updated pieces of documentation on the HTTP server, Zigbee2mqtt and mpd. Also added example dashboard template and event hook script.
This commit is contained in:
parent
151d0008ee
commit
ffb7a3e5a3
6 changed files with 272 additions and 119 deletions
|
@ -16,21 +16,21 @@
|
|||
# Using multiple files is encouraged in the case of large configurations
|
||||
# that can easily end up in a messy config.yaml file, as they help you
|
||||
# keep your configuration more organized.
|
||||
include:
|
||||
- include/logging.yaml
|
||||
- include/media.yaml
|
||||
- include/sensors.yaml
|
||||
#include:
|
||||
# - include/logging.yaml
|
||||
# - include/media.yaml
|
||||
# - include/sensors.yaml
|
||||
|
||||
# platypush logs on stdout by default. You can use the logging section to specify
|
||||
# an alternative file or change the logging level.
|
||||
logging:
|
||||
filename: ~/.local/log/platypush/platypush.log
|
||||
level: INFO
|
||||
#logging:
|
||||
# filename: ~/.local/log/platypush/platypush.log
|
||||
# level: INFO
|
||||
|
||||
# The device_id is used by many components of platypush and it should uniquely
|
||||
# identify a device in your network. If nothing is specified then the hostname
|
||||
# will be used.
|
||||
device_id: myname
|
||||
#device_id: my_device
|
||||
|
||||
## --
|
||||
## Plugin configuration examples
|
||||
|
@ -46,31 +46,31 @@ device_id: myname
|
|||
# https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html
|
||||
# for reference. You can easily install the required dependencies for the plugin through
|
||||
# pip install 'platypush[hue]'
|
||||
light.hue:
|
||||
# IP address or hostname of the Hue bridge
|
||||
bridge: 192.168.1.10
|
||||
# Groups that will be handled by default if nothing is specified on the request
|
||||
groups:
|
||||
- Living Room
|
||||
#light.hue:
|
||||
# # IP address or hostname of the Hue bridge
|
||||
# bridge: 192.168.1.10
|
||||
# # Groups that will be handled by default if nothing is specified on the request
|
||||
# groups:
|
||||
# - Living Room
|
||||
|
||||
# Example configuration of music.mpd plugin, see
|
||||
# https://docs.platypush.tech/en/latest/platypush/plugins/music.mpd.html
|
||||
# You can easily install the dependencies through pip install 'platypush[mpd]'
|
||||
music.mpd:
|
||||
host: localhost
|
||||
port: 6600
|
||||
#music.mpd:
|
||||
# host: localhost
|
||||
# port: 6600
|
||||
|
||||
# Example configuration of media.chromecast plugin, see
|
||||
# https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html
|
||||
# You can easily install the dependencies through pip install 'platypush[chromecast]'
|
||||
media.chromecast:
|
||||
chromecast: Living Room TV
|
||||
#media.chromecast:
|
||||
# chromecast: Living Room TV
|
||||
|
||||
# Plugins with empty configuration can also be explicitly enabled by specifying
|
||||
# enabled=True or disabled=False (it's a good practice if you want the
|
||||
# corresponding web panel to be enabled, if available)
|
||||
camera:
|
||||
enabled: True
|
||||
#camera:
|
||||
# enabled: True
|
||||
|
||||
# Support for last.fm scrobbling. Install dependencies with 'pip install "platypush[lastfm]"
|
||||
lastfm:
|
||||
|
@ -81,13 +81,11 @@ lastfm:
|
|||
|
||||
# Support for calendars - in this case Google and Facebook calendars
|
||||
# Installing the dependencies: pip install 'platypush[ical,google]'
|
||||
calendar:
|
||||
calendars:
|
||||
-
|
||||
type: platypush.plugins.google.calendar.GoogleCalendarPlugin
|
||||
-
|
||||
type: platypush.plugins.calendar.ical.CalendarIcalPlugin
|
||||
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
|
||||
#calendar:
|
||||
# calendars:
|
||||
# - type: platypush.plugins.google.calendar.GoogleCalendarPlugin
|
||||
# - type: platypush.plugins.calendar.ical.CalendarIcalPlugin
|
||||
# url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
|
||||
|
||||
## --
|
||||
## Backends configuration examples
|
||||
|
@ -118,77 +116,49 @@ calendar:
|
|||
backend.http:
|
||||
# Listening port
|
||||
port: 8008
|
||||
# Websocket port
|
||||
websocket_port: 8009
|
||||
|
||||
# Through resource_dirs you can specify external folders whose content can be accessed on
|
||||
# the web server through a custom URL. In the case below we have a Dropbox folder containing
|
||||
# our pictures and we mount it to the '/carousel' endpoint.
|
||||
resource_dirs:
|
||||
carousel: ~/Dropbox/Photos/carousel
|
||||
|
||||
# Dashboard configuration. The dashboard is a collection of widgets and it's organized in
|
||||
# multiple rows. Each rows can be split in 12 columns. Therefore 'columns: 12' will make
|
||||
# a widget span over the whole row, while 'columns: 6' will make a widget take half the
|
||||
# horizontal space of a column.
|
||||
dashboard:
|
||||
widgets:
|
||||
-
|
||||
widget: calendar
|
||||
columns: 6
|
||||
-
|
||||
widget: music
|
||||
columns: 3
|
||||
-
|
||||
widget: date-time-weather
|
||||
columns: 3
|
||||
-
|
||||
widget: image-carousel
|
||||
columns: 6
|
||||
images_path: ~/Dropbox/Photos/carousel
|
||||
refresh_seconds: 15
|
||||
-
|
||||
widget: rss-news
|
||||
# Requires backend.http.poll to be enabled with some
|
||||
# RSS sources and write them to sqlite db
|
||||
columns: 6
|
||||
limit: 25
|
||||
db: "sqlite:////home/user/.local/share/platypush/feeds/rss.db"
|
||||
carousel: /mnt/hd/photos/carousel
|
||||
|
||||
# The HTTP poll backend is a versatile backend that can monitor for HTTP-based resources and
|
||||
# trigger events whenever new entries are available. In the example below we show how to use
|
||||
# the backend to listen for changes on a set of RSS feeds. New content will be stored by default
|
||||
# on a SQLite database under ~/.local/share/platypush/feeds/rss.db.
|
||||
# Install the required dependencies through 'pip install "platypush[rss,db]"'
|
||||
backend.http.poll:
|
||||
requests:
|
||||
-
|
||||
# HTTP poll type (RSS)
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
# Remote URL
|
||||
url: http://www.theguardian.com/rss/world
|
||||
# Custom title
|
||||
title: The Guardian - World News
|
||||
# How often we should check for changes
|
||||
poll_seconds: 600
|
||||
# Maximum number of new entries to be processed
|
||||
max_entries: 10
|
||||
-
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://www.physorg.com/rss-feed
|
||||
title: Phys.org
|
||||
poll_seconds: 600
|
||||
max_entries: 10
|
||||
-
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://feeds.feedburner.com/Techcrunch
|
||||
title: Tech Crunch
|
||||
poll_seconds: 600
|
||||
max_entries: 10
|
||||
-
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||
title: The New York Times
|
||||
poll_seconds: 300
|
||||
max_entries: 10
|
||||
#backend.http.poll:
|
||||
# requests:
|
||||
# - type: platypush.backend.http.request.rss.RssUpdates # HTTP poll type (RSS)
|
||||
# # Remote URL
|
||||
# url: http://www.theguardian.com/rss/world
|
||||
# # Custom title
|
||||
# title: The Guardian - World News
|
||||
# # How often we should check for changes
|
||||
# poll_seconds: 600
|
||||
# # Maximum number of new entries to be processed
|
||||
# max_entries: 10
|
||||
#
|
||||
# - type: platypush.backend.http.request.rss.RssUpdates
|
||||
# url: http://www.physorg.com/rss-feed
|
||||
# title: Phys.org
|
||||
# poll_seconds: 600
|
||||
# max_entries: 10
|
||||
#
|
||||
# - type: platypush.backend.http.request.rss.RssUpdates
|
||||
# url: http://feeds.feedburner.com/Techcrunch
|
||||
# title: Tech Crunch
|
||||
# poll_seconds: 600
|
||||
# max_entries: 10
|
||||
#
|
||||
# - type: platypush.backend.http.request.rss.RssUpdates
|
||||
# url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||
# title: The New York Times
|
||||
# poll_seconds: 300
|
||||
# max_entries: 10
|
||||
|
||||
# MQTT backend. Installed required dependencies through 'pip install "platypush[mqtt]"'
|
||||
backend.mqtt:
|
||||
|
@ -196,15 +166,15 @@ backend.mqtt:
|
|||
host: mqtt-server
|
||||
# By default the backend will listen for messages on the platypush_bus_mq/device_id
|
||||
# topic, but you can change the prefix using the topic attribute
|
||||
topic: my_platypush_bus
|
||||
# topic: MyBus
|
||||
|
||||
# Raw TCP socket backend. It can run commands sent as JSON over telnet or netcat
|
||||
backend.tcp:
|
||||
port: 3333
|
||||
#backend.tcp:
|
||||
# port: 3333
|
||||
|
||||
# Websocket backend. Install required dependencies through 'pip install "platypush[http]"'
|
||||
backend.websocket:
|
||||
port: 8765
|
||||
#backend.websocket:
|
||||
# port: 8765
|
||||
|
||||
## --
|
||||
## Assistant configuration examples
|
||||
|
@ -254,9 +224,12 @@ backend.assistant.snowboy:
|
|||
assistant.echo:
|
||||
audio_player: mplayer
|
||||
|
||||
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant]"'
|
||||
assistant.google.pushtotalk:
|
||||
language: en-US
|
||||
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant-legacy]"'
|
||||
assistant.google:
|
||||
enabled: True
|
||||
|
||||
backend.assistant.google:
|
||||
enabled: True
|
||||
|
||||
## --
|
||||
## Procedure examples
|
||||
|
@ -327,14 +300,14 @@ procedure.outside_home:
|
|||
procedure.send_request(target, action, args):
|
||||
- action: mqtt.send_message
|
||||
args:
|
||||
topic: my_platypush_bus/${target}
|
||||
topic: platypush_bus_mq/${target}
|
||||
host: mqtt-server
|
||||
port: 1883
|
||||
msg:
|
||||
type: request
|
||||
target: ${target}
|
||||
action: ${action}
|
||||
args: "${context.get('args', {}}"
|
||||
args: ${args}
|
||||
|
||||
## --
|
||||
## Event hook examples
|
||||
|
@ -418,4 +391,3 @@ cron.TestCron:
|
|||
- action: shell.exec
|
||||
args:
|
||||
cmd: ~/bin/myscript.sh
|
||||
|
||||
|
|
33
examples/conf/dashboard.xml
Normal file
33
examples/conf/dashboard.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<!-- Dashboard templates are stored as ~/.config/platypush/dashboards/<name>.xml and can be accessed on
|
||||
http://<host>:8008/dashboard/<name>. A dashboard can show a custom set of widgets on a screen - e.g. calendar
|
||||
events, media information, photo carousels, sensors data, weather forecast and news headlines. The available
|
||||
widgets are stored as Vue.js templates under `platypush/backend/http/webapp/src/components/widgets`. -->
|
||||
<Dashboard>
|
||||
<!-- Display the following widgets on the same row. Each row consists of 12 columns.
|
||||
You can specify the width of each widget either through class name (e.g. col-6 means
|
||||
6 columns out of 12, e.g. half the size of the row) or inline style
|
||||
(e.g. `style="width: 50%"`). -->
|
||||
<Row>
|
||||
<!-- Show a calendar widget with the upcoming events. It requires the `calendar` plugin to
|
||||
be enabled and configured. -->
|
||||
<Calendar class="col-6" />
|
||||
|
||||
<!-- Show the current track and other playback info. It requires `music.mpd` plugin or any
|
||||
other music plugin enabled. -->
|
||||
<Music class="col-3" />
|
||||
|
||||
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
|
||||
<DateTimeWeather class="col-3" />
|
||||
</Row>
|
||||
|
||||
<!-- Display the following widgets on a second row -->
|
||||
<Row>
|
||||
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
||||
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
|
||||
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
||||
|
||||
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
||||
`http.poll` backend -->
|
||||
<RssNews class="col-6" db="sqlite:////path/to/your/rss.db" />
|
||||
</Row>
|
||||
</Dashboard>
|
43
examples/conf/hook.py
Normal file
43
examples/conf/hook.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# A more versatile way to define event hooks than the YAML format of `config.yaml` is through native Python scripts.
|
||||
# You can define hooks as simple Python functions that use the `platypush.event.hook.hook` decorator to specify on
|
||||
# which event type they should be called, and optionally on which event attribute values.
|
||||
#
|
||||
# Event hooks should be stored in Python files under `~/.config/platypush/scripts`. All the functions that use the
|
||||
# @hook decorator will automatically be discovered and imported as event hooks into the platform at runtime.
|
||||
|
||||
# `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
|
||||
from platypush.utils import run
|
||||
|
||||
# @hook decorator
|
||||
from platypush.event.hook import hook
|
||||
|
||||
# Event types that you want to react to
|
||||
from platypush.message.event.assistant import ConversationStartEvent, SpeechRecognizedEvent
|
||||
|
||||
|
||||
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
|
||||
def on_music_play_command(event, title=None, artist=None, **context):
|
||||
"""
|
||||
This function will be executed when a SpeechRecognizedEvent with `phrase="play the music"` is triggered.
|
||||
`event` contains the event object and `context` any key-value info from the running context.
|
||||
Note that in this specific case we can leverage the token-extraction feature of SpeechRecognizedEvent through
|
||||
${} that operates on regex-like principles to extract any text that matches the pattern into context variables.
|
||||
"""
|
||||
results = run('music.mpd.search', filter={
|
||||
'artist': artist,
|
||||
'title': title,
|
||||
})
|
||||
|
||||
if results:
|
||||
run('music.mpd.play', results[0]['file'])
|
||||
else:
|
||||
run('tts.say', "I can't find any music matching your query")
|
||||
|
||||
|
||||
@hook(ConversationStartEvent)
|
||||
def on_conversation_start(event, **context):
|
||||
"""
|
||||
A simple hook that gets invoked when a new conversation starts with a voice assistant and simply pauses the music
|
||||
to make sure that your speech is properly detected.
|
||||
"""
|
||||
run('music.mpd.pause_if_playing')
|
|
@ -12,36 +12,137 @@ from platypush.utils import get_ssl_server_context, set_thread_name
|
|||
|
||||
class HttpBackend(Backend):
|
||||
"""
|
||||
The HTTP backend is a general-purpose web server that you can leverage:
|
||||
The HTTP backend is a general-purpose web server.
|
||||
|
||||
* To execute Platypush commands via HTTP calls. Example::
|
||||
Example configuration:
|
||||
|
||||
curl -XPOST -H 'Content-Type: application/json' -H "X-Token: your_token" \\
|
||||
-d '{
|
||||
.. code-block:: yaml
|
||||
|
||||
backend.http:
|
||||
# Default HTTP listen port
|
||||
port: 8008
|
||||
# Default websocket port
|
||||
websocket_port: 8009
|
||||
# External folders that will be exposed over `/resources/<name>`
|
||||
resource_dirs:
|
||||
photos: /mnt/hd/photos
|
||||
videos: /mnt/hd/videos
|
||||
music: /mnt/hd/music
|
||||
|
||||
You can leverage this backend:
|
||||
|
||||
* To execute Platypush commands via HTTP calls. In order to do so:
|
||||
|
||||
** Register a user to Platypush through the web panel (usually served on ``http://host:8008/``).
|
||||
|
||||
** Generate a token for your user, either through the web panel (Settings -> Generate Token) or via API:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
curl -XPOST -H 'Content-Type: application/json' -d '
|
||||
{
|
||||
"username": "$YOUR_USER",
|
||||
"password": "$YOUR_PASSWORD"
|
||||
}' http://host:8008/auth
|
||||
|
||||
** Execute actions through the ``/execute`` endpoint:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||
{
|
||||
"type": "request",
|
||||
"target":"nodename",
|
||||
"action": "tts.say",
|
||||
"args": {"phrase":"This is a test"}
|
||||
}' \\
|
||||
http://localhost:8008/execute
|
||||
"args": {
|
||||
"text": "This is a test"
|
||||
}
|
||||
}' http://localhost:8008/execute
|
||||
|
||||
* To interact with your system (and control plugins and backends) through the Platypush web panel,
|
||||
by default available on your web root document. Any plugin that you have configured and available as a panel
|
||||
plugin will appear on the web panel as well as a tab.
|
||||
by default available on ``http://host:8008/``. Any configured plugin that has an available panel
|
||||
plugin will be automatically added to the web panel.
|
||||
|
||||
* To display a fullscreen dashboard with your configured widgets, by default available under ``/dashboard/<dashboard_name>``
|
||||
* To display a fullscreen dashboard with custom widgets.
|
||||
|
||||
* To stream media over HTTP through the ``/media`` endpoint
|
||||
** Widgets are available as Vue.js components under
|
||||
``platypush/backend/http/webapp/src/components/widgets``.
|
||||
|
||||
** Explore their options (some may require some plugins or backends to be configured in order to work) and
|
||||
create a new dashboard template under ``~/.config/platypush/dashboards``- e.g. ``main.xml``:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<Dashboard>
|
||||
<!-- Display the following widgets on the same row. Each row consists of 12 columns.
|
||||
You can specify the width of each widget either through class name (e.g. col-6 means
|
||||
6 columns out of 12, e.g. half the size of the row) or inline style
|
||||
(e.g. `style="width: 50%"`). -->
|
||||
<Row>
|
||||
<!-- Show a calendar widget with the upcoming events. It requires the `calendar` plugin to
|
||||
be enabled and configured. -->
|
||||
<Calendar class="col-6" />
|
||||
|
||||
<!-- Show the current track and other playback info. It requires `music.mpd` plugin or any
|
||||
other music plugin enabled. -->
|
||||
<Music class="col-3" />
|
||||
|
||||
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
|
||||
<DateTimeWeather class="col-3" />
|
||||
</Row>
|
||||
|
||||
<!-- Display the following widgets on a second row -->
|
||||
<Row>
|
||||
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
||||
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
|
||||
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
||||
|
||||
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
||||
`http.poll` backend -->
|
||||
<RssNews class="col-6" db="sqlite:////path/to/your/rss.db" />
|
||||
</Row>
|
||||
</Dashboard>
|
||||
|
||||
** The dashboard will be accessible under ``http://host:8008/dashboard/<name>``, where ``name=main`` if for
|
||||
example you stored your template under ``~/.config/platypush/dashboards/main.xml``.
|
||||
|
||||
* To expose custom endpoints that can be called as web hooks by other applications and run some custom logic.
|
||||
All you have to do in this case is to create a hook on a
|
||||
:class:`platypush.message.event.http.hook.WebhookEvent` with the endpoint that you want to expose and store
|
||||
it under e.g. ``~/.config/platypush/scripts/hooks.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from platypush.context import get_plugin
|
||||
from platypush.event.hook import hook
|
||||
from platypush.message.event.http.hook import WebhookEvent
|
||||
|
||||
hook_token = 'abcdefabcdef'
|
||||
|
||||
# Expose the hook under the /hook/lights_toggle endpoint
|
||||
@hook(WebhookEvent, hook='lights_toggle')
|
||||
def lights_toggle(event, **context):
|
||||
# Do any checks on the request
|
||||
assert event.headers.get('X-Token') == hook_token, 'Unauthorized'
|
||||
|
||||
# Run some actions
|
||||
lights = get_plugin('light.hue')
|
||||
lights.toggle()
|
||||
|
||||
Any plugin can register custom routes under ``platypush/backend/http/app/routes/plugins``.
|
||||
Any additional route is managed as a Flask blueprint template and the `.py`
|
||||
module can expose lists of routes to the main webapp through the
|
||||
``__routes__`` object (a list of Flask blueprints).
|
||||
|
||||
Note that if you set up a main token, it will be required for any HTTP
|
||||
interaction - either as ``X-Token`` HTTP header, on the query string
|
||||
(attribute name: ``token``), as part of the JSON payload root (attribute
|
||||
name: ``token``), or via HTTP basic auth (any username works).
|
||||
Security: Access to the endpoints requires at least one user to be registered. Access to the endpoints is regulated
|
||||
in the following ways (with the exception of event hooks, whose logic is up to the user):
|
||||
|
||||
* **Simple authentication** - i.e. registered username and password.
|
||||
* **JWT token** provided either over as ``Authorization: Bearer`` header or ``GET`` ``?token=<TOKEN>``
|
||||
parameter. A JWT token can be generated either through the web panel or over the ``/auth`` endpoint.
|
||||
* **Global platform token**, usually configured on the root of the ``config.yaml`` as ``token: <VALUE>``.
|
||||
It can provided either over on the ``X-Token`` header or as a ``GET`` ``?token=<TOKEN>`` parameter.
|
||||
* **Session token**, generated upon login, it can be used to authenticate requests through the ``Cookie`` header
|
||||
(cookie name: ``session_token``).
|
||||
|
||||
Requires:
|
||||
|
||||
|
|
|
@ -16,9 +16,13 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
sources through plugins (e.g. Spotify, TuneIn, Soundcloud, local files
|
||||
etc.).
|
||||
|
||||
**NOTE**: As of Mopidy 3.0 MPD is an optional interface provided by the ``mopidy-mpd`` extension. Make sure that you
|
||||
have the extension installed and enabled on your instance to use this plugin with your server.
|
||||
|
||||
Requires:
|
||||
|
||||
* **python-mpd2** (``pip install python-mpd2``)
|
||||
|
||||
"""
|
||||
|
||||
_client_lock = threading.RLock()
|
||||
|
|
|
@ -33,8 +33,8 @@ class ZigbeeMqttPlugin(MqttPlugin, SwitchPlugin):
|
|||
|
||||
.. code-block:: shell
|
||||
|
||||
wget https://github.com/Koenkk/Z-Stack-firmware/raw/master/coordinator/Z-Stack_Home_1.2/bin/default/CC2531_DEFAULT_20190608.zip
|
||||
unzip CC2531_DEFAULT_20190608.zip
|
||||
wget https://github.com/Koenkk/Z-Stack-firmware/raw/master/coordinator/Z-Stack_Home_1.2/bin/default/CC2531_DEFAULT_20201127.zip
|
||||
unzip CC2531_DEFAULT_20201127.zip
|
||||
[sudo] cc-tool -e -w CC2531ZNP-Prod.hex
|
||||
|
||||
- You can disconnect your debugger and downloader cable once the firmware is flashed.
|
||||
|
|
Loading…
Reference in a new issue