Rearrange docu in /help and correct some examples

This commit is contained in:
fauresystems
2019-11-15 19:27:07 +01:00
parent ea0a9425d1
commit 447f33f62c
29 changed files with 157 additions and 96 deletions
+101
View File
@@ -0,0 +1,101 @@
# Arduino Props for Escape Room 2.0
The *ArduinoProps library* for Arduino has been designed to build *escape room 2.0 props*.
Specificities of props for escape rooms:
- very high uptime (near 100%)
- fast problem fixing
- real-time (human scale) notification and control
- fallback controls
- low Total Cost of Ownership
Benefits and specificities of the *ArduinoProps library*
- rely on standard MQTT messaging protocol for IoT
- simplify and ease Arduino coding
- constant sustaining of Arduino code (YUN)
- real-time notification, monitoring and control (human scale)
# Escape room 2.0 constraints on props
Escape game are opened 7 days a week and particularly booked during weekends and usually until midnight. Therefore their uptime must be close to 100% and incidents must be worked-around immediately by the game master (manual commands).
Escape Room Props operation and performance impact directly escape game customer experience. Therefore notifications must be processed in human-scale real-time, automatically or manually by a non-technological user (the game master).
Escape Room Props TCO (Total Cost of Ownership) must be affordable by escape games (SOHO costs for small businesses), processing is usually supported by Arduino or Raspberry boards. IT professional 24/7 support must be avoided for cost reasons. Therefore props code must be the most reliable.
# *ArduinoProps library* for escape room 2.0
[MQTT](http://mqtt.org/) is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging transport.
*ArduinoProps library* relies on *[Nick O'Leary PubSubClient](https://github.com/knolleary/pubsubclient)*, a simple publish/subscribe messaging Arduino library. [PubSubClient](https://github.com/knolleary/pubsubclient) is a MQTT client with a few limitations:
- It can only publish QoS 0 messages. It can subscribe at QoS 0 or QoS 1.
- The maximum message size, including header, is 128 bytes by default. This is configurable via MQTT_MAX_PACKET_SIZE in PubSubClient.h.
- The keepalive interval is set to 15 seconds by default. This is configurable via MQTT_KEEPALIVE in PubSubClient.h.
- The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by changing value of MQTT_VERSION in PubSubClient.h.
How does *ArduinoProps library* use MQTT efficiently in escape rooms:
- Every props has two MQTT topics assigned as inbox and outbox.
- More MQTT topics can be used to share game countdown, scenario or any common useful dynamic data for the escape game.
- Notifying the props is done by publishing a message on its inbox topic.
- Other props or software controllers can subscribe to the props outbox topic to receive all its notifications.
- *ArduinoProps library* includes a ping process at application level, in LAN network the round-trip-delay is around 50 milliseconds.
- *ArduinoProps library* is defaulted to publish QoS 0 messages and to subscribe at QoS 1.
*ArduinoProps library* wraps [PubSubClient](https://github.com/knolleary/pubsubclient) to make Arduino code the most simple and readable for better sustainability.
# Arduino YUN and YUN shields
Arduino YUN is a Arduino with Leonardo MCU/GPIO and Linux SoC.
Arduino YUN shield and Dragino YUN shield are YUN Linux SoC to be plugged onto Arduino Uno, Mega or Due.
**Arduino YUN advantage is that you can upload AVR program from the Arduino IDE at any moment during the Props lifetime**:
- change sensor settings when mechanics is undermined
- tune whole challenge processing
Arduino YUN can be rebooted or its MCU resetted remotely with simple SSH commands.
Moreover, SSH command can be used to remotely change the MQTT server IP address, *ArduinoProps library* read this address at sketch start.
Therefore, **Arduino YUN or YUN shield is the preferred solution for props with Arduino boards** because it will be very easy to sustain over time and greatly facilitates fallback plans to ensure maximum uptime:
- **Arduino Yún**
- **Arduino Yún Rev 2**
- **Arduino Mega 2560 Rev3 + Dragino Yún Shield**
- **Elegoo Mega 2560 R3 + Dragino Yún Shield**
- **Arduino Uno R3 + Dragino Yún Shield**
- **Arduino Due + Dragino Yún Shield**
# Arduino Ethernet and Wifi
Arduino with Ethernet or Wifi shield is suitable to build an Escape Room Props, however the sketch must be uploaded with an USB cable, so it is not convenient for props design and worse for props in production.
# Compatible hardware
*ArduinoProps library* supported harware:
- Arduino Yún and Arduino Yún Rev 2
- Any Arduino + Dragino Yún Shield (tested with Elegoo Mega 2560 R3)
- Arduino Uno WiFi Rev 2
- Arduino NANO 33 IoT
- Arduino MKR WiFi 1010
- Arduino MKR VIDOR 4000
- Arduino Ethernet
- Arduino Ethernet Shield
For WiFi props, please update the WiFiNINA firmware: [WiFiNINA firmware update](WifiNinaFirmware.md).
To use other WiFi hardware compatible with any *<a href="https://github.com/knolleary/pubsubclient" target="_blank">Nick O'Leary PubSubClient</a>* library you have to fork `WifiProps.h` and `WifiProps.cpp` and to replace `WiFiNINA.h` whih appropriate WiFi library (`ESP8266WiFi.h` for example):
- Arduino WiFi Shield
- Sparkfun WiFly Shield [library](https://github.com/dpslwk/WiFly)
- TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library)
- Intel Galileo/Edison
- ESP8266
- ESP32
## Author
**Marie FAURE** (Oct 18th, 2019)
* company: FAURE SYSTEMS SAS
* mail: *dev at faure dot systems*
* github: <a href="https://github.com/fauresystems?tab=repositories" target="_blank">fauresystems</a>
* web: <a href="https://www.live-escape.net/" target="_blank">Live Escape Grenoble</a>
+351
View File
@@ -0,0 +1,351 @@
# *Props* class reference
*Props* is a base class for 3 connected props, depending on their network interface:
* ***BridgeProps*** for Yun board and Yun shield
* ***EthernetProps*** for Ethernet shield
* ***WifiProps*** for WiFiNINA boards
*Props* class reference:
1. Define the *Props*
* Defining a *BridgeProps*
* Defining an *EthernetProps*
* Defining a *WifiProps*
2. Setup the *Props*
* Change timings
3. Call in sketch *loop()*
4. Implement `InboxMessage::run()` callback
5. Update broker address from SSH (*BridgeProps*)
6. Examples
7. Compatible hardware
## 1. Defines the *Props*
Defining a Bridge (Yun), Ethernet or Wifi *Props* is slighlty different.
### Defining a *BridgeProps*
```csharp
#include <Bridge.h>
#include "ArduinoProps.h"
// If you're running our Escape Room control software (Room 2.0) you have to respect
// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox
// https://live-escape.net/go/room
BridgeProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker
u8"Room/Demoniak/Props/Arduino Contrôleur/inbox",
u8"Room/Demoniak/Props/Arduino Contrôleur/outbox",
"192.168.1.42", // your MQTT server IP address
1883); // your MQTT server port;
PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true);
PropsDataLogical led(u8"led");
void clignote(); // forward
PropsAction clignoteAction = PropsAction(1000, clignote);
```
Create *BridgeProps* instance with MQTT parameters, then create *PropsData* instances and *PropsActions*.
The *Bridge* instance is created by `#include <Bridge.h>`.
### Defining an *EthernetProps*
```csharp
#include <Ethernet.h>
#include <IPAddress.h>
#include "ArduinoProps.h"
byte mac[] = { 0xFC, 0xFA, 0xEA, 0x00, 0x00, 0x02 }; //<<< MAKE SURE IT'S UNIQUE IN YOUR NETWORK!!!
String ip = "192.168.1.19"; //<<< ENTER YOUR IP ADDRESS HERE ("" for DHCP)
// Builtin led is not available with the shield
#undef LED_BUILTIN
#define LED_BUILTIN 8
// If you're running our Escape Room control software (Room 2.0) you have to respect
// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox
// https://live-escape.net/go/room
EthernetProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker
u8"Room/Demoniak/Props/Arduino Contrôleur/inbox",
u8"Room/Demoniak/Props/Arduino Contrôleur/outbox",
"192.168.1.42", // your MQTT server IP address
1883); // your MQTT server port;
PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true);
PropsDataLogical led(u8"led");
void clignote(); // forward
PropsAction clignoteAction = PropsAction(1000, clignote);
```
Create MAC address (which must be unique, it's the hardware ID in the netwprk).
Create *EthernetProps* instance with MQTT parameters, then create *PropsData* instances and *PropsActions*.
The *Ethernet* instance is created by `#include <Ethernet.h>`.
### Defining a *WifiProps*
```csharp
#include "ArduinoProps.h"
// Setup your WiFi network
const char* ssid = "";
const char *passphrase = "";
// Builtin led is not available with the shield
#undef LED_BUILTIN
#define LED_BUILTIN 8
// If you're running our Escape Room control software (Room 2.0) you have to respect
// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox
// https://live-escape.net/go/room
WifiProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker
u8"Room/Demoniak/Props/Arduino Contrôleur/inbox",
u8"Room/Demoniak/Props/Arduino Contrôleur/outbox",
"192.168.1.42", // your MQTT server IP address
1883); // your MQTT server port;
PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true);
PropsDataLogical led(u8"led");
void clignote(); // forward
PropsAction clignoteAction = PropsAction(1000, clignote);
```
The *AduinoProps* library uses **<a href="https://github.com/arduino-libraries/WiFiNINA" target="_blank">WiFiNINA</a>** WiFi library for **Arduino Uno WiFi Rev 2**, **Arduino NANO 33 IoT**, **Arduino MKR WiFi 1010** and **Arduino MKR VIDOR 4000**.
Please update the WiFiNINA firmware: [WiFiNINA firmware update](WifiNinaFirmware.md).
Create *WifiProps* instance with MQTT parameters, then create *PropsData* instances and *PropsActions*.
#### MQTT topics
The props listens to the *inbox* topic for command messages and send data and messages into the *outbox* topic.
If you're running Live Escape Room 2.0 control software, you have to respect *inbox*/*outbox* syntax:
Room 2.0 *inbox* topic:
```csharp
format: Room/[escape room name]/Props/[props name]/inbox
example: u8"Room/Demoniak/Props/Arduino Contrôleur/inbox"
```
Room 2.0 *outbox* topic:
```csharp
format: Room/[escape room name]/Props/[props name]/outbox
example: u8"Room/Demoniak/Props/Arduino Contrôleur/outbox"
```
See [README.md](../README.md): **4. Application protocol for escape room 2.0 props**.
#### *PropsData* instances
*PropsData* provides a facility to monitor data sent into MQTT outbox.
See [PropsData.md](PropsData.md) for *PropsData* reference.
#### *PropsAction* instances
To maintain a non-blocking asynchronous behavior of your props, use *PropsAction* objects to trigger action periodically.
Reading I/O and simple computing (arithmetics) can be done at every sketch loop but long computing have to run periodically.
For example, to blink a led, don't block the skecth with a `delay(1000)` call to switch the output: instead, ***check if 1 second is elapsed to switch the output***.
See [PropsAction.md](PropsData.md) for *PropsAction* reference.
## 2. Setup the *Props*
`InboxMessage::run` is the callback executed when a message is received in the MQTT inbox.
```csharp
void setup()
{
// CODE TO BEGIN THE INTERNET CONNECTION
... see below the different code for BridgeProps, EthenetProps and WifiProps
// SAME CODE FOR ALL PROPS
props.addData(&clignoter);
props.addData(&led);
props.begin(InboxMessage::run);
pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output
// At this point, the broker is not connected yet
}
```
*BridgeProps*, *EthenetProps* and *WifiProps* has the exactly same setup:
- add *PropsData* references to the props
- call `props.begin(InboxMessage::run)`
The setup code is slighlty different to start the network client (bridge, ethernet of wifi):
```csharp
void setup()
{
#if defined(BRIDGEPROPS_H)
Bridge.begin();
updateBrokerAdressFromFile("/root/broker", &props); // if you're running our Escape Room control software (Room 2.0)
#elif defined(ETHERNETPROPS_H)
// can do more static IP configuration
//Ethernet.setSubnetMask(IPAddress());
//Ethernet.setGatewayIP(IPAddress());
//Ethernet.setDnsServerIP(IPAddress());
IPAddress ipa;
if (!ipa.fromString(ip)) {
// attempt a DHCP connection:
if (!Ethernet.begin(mac)) {
// if DHCP fails, start with a hard-coded address:
Ethernet.begin(mac, IPAddress(10, 90, 90, 239));
}
}
else {
Ethernet.begin(mac, ipa);
}
delay(1500); // time for shield stuff
#elif defined(WIFIPROPS_H)
// can do more static IP configuration
//WiFi.config(IPAddress(), IPAddress(), IPAddress());
//WiFi.config(IPAddress(), IPAddress());
//WiFi.config(IPAddress());
WiFi.begin(ssid, passphrase);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
#endif
...
}
```
### Change timings
Default timings are:
* check data changes every 400 milliseconds
* *Props* remains silent for up to 30 seconds
Timings can be changed anywhere in the code with `resetIntervals()`:
```csharp
props.resetIntervals(250, 15); // check data changes every 250 milliseconds, silent up to 15 seconds
```
## 3. Call in sketch *loop()*
```csharp
void loop()
{
props.loop();
led.setValue(digitalRead(LED_BUILTIN)); // read I/O
clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls
}
```
Couldn't be more simple!
Just call `props.loop()` when entering the sketch `loop()`, it will maintain MQTT broker connection and run callback when receiving MQTT messages.
## 4. Implement `InboxMessage::run()` callback
`InboxMessage::run` is the callback executed when a message is received in the MQTT inbox, you have to implement it.
Example for Room 2.0 commands:
```csharp
void InboxMessage::run(String a) {
if (a == u8"app:startup")
{
props.sendAllData();
props.sendDone(a);
}
else if (a == "clignoter:1")
{
clignoter.setValue(true);
props.sendAllData(); // all data change, we don't have to be selctive then
props.sendDone(a); // acknowledge props command action
}
else if (a == "clignoter:0")
{
clignoter.setValue(false);
props.sendAllData(); // all data change, we don't have to be selctive then
props.sendDone(a); // acknowledge props command action
}
else
{
// acknowledge omition of the props command
props.sendOmit(a);
}
}
```
## 5. Update broker address from SSH (*BridgeProps*)
Room 2.0 use a SSH command to change remotely the broker IP address (in /root/broker file) and to relaunch to sketch:
```bash
ssh 'echo 192.168.1.14> /root/broker && reset-mcu'
```
On the *BridgeProps* you can read the broker IP address from Linino filesystem with this code:
```csharp
void updateBrokerAdressFromFile(const char* broker_file, BridgeProps* props)
{
// broker IP address is stored in Linino file systems and updated with ssh command by Room 2.0
IPAddress ip;
Process _process;
_process.begin("cat");
_process.addParameter(broker_file); // for ssh remotely set broker address
_process.run(); // run the process and wait for its termination
String b;
while (_process.available() > 0) {
char c = _process.read();
b += c;
}
b.trim();
if (ip.fromString(b.c_str())) props->setBrokerIpAddress(ip);
}
```
## 6. Examples
The library comes with a number of example sketches. See **File > Examples > ArduinoProps** within the Arduino IDE application.
See [EXAMPLES.md](../EXAMPLES.md) for an adaptation of the Blink example (https://www.arduino.cc/en/tutorial/blink) as a simple MQTT props:
1. **BlinkOnBridgeProps**: the Blink example on a Yun props with *ArduinoProps library*
2. **BlinkOnEthernetProps**: the Blink example on an Ethernet props with *ArduinoProps library*
3. **BlinkOnWifiProps**: the Blink example on a Wifi props with *ArduinoProps library*
4. **BlinkOnBridgePubSub**: the Blink example on props using *PubSubClient* directly
## 7. Compatible hardware
*Props library* supported harware:
- Arduino Yún and Arduino Yún Rev 2
- Any Arduino + Dragino Yún Shield (tested with Elegoo Mega 2560 R3)
- Arduino Uno WiFi Rev 2
- Arduino NANO 33 IoT
- Arduino MKR WiFi 1010
- Arduino MKR VIDOR 4000
- Arduino Ethernet
- Arduino Ethernet Shield
For WiFi props, please update the WiFiNINA firmware: [WiFiNINA firmware update](WifiNinaFirmware.md).
To use other WiFi hardware compatible with any *<a href="https://github.com/knolleary/pubsubclient" target="_blank">Nick O'Leary PubSubClient</a>* library you have to fork `WifiProps.h` and `WifiProps.cpp` and to replace `WiFiNINA.h` whih appropriate WiFi library (`ESP8266WiFi.h` for example):
- Arduino WiFi Shield
- Sparkfun WiFly Shield [library](https://github.com/dpslwk/WiFly)
- TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library)
- Intel Galileo/Edison
- ESP8266
- ESP32
## Author
**Marie FAURE** (Oct 18th, 2019)
* company: FAURE SYSTEMS SAS
* mail: *dev at faure dot systems*
* github: <a href="https://github.com/fauresystems?tab=repositories" target="_blank">fauresystems</a>
* web: <a href="https://www.live-escape.net/" target="_blank">Live Escape Grenoble</a>
+57
View File
@@ -0,0 +1,57 @@
# *PropsAction* class reference
*PropsAction* brings asynchronous-like behavior to sketch `loop()`.
## Constructors
* `PropsAction()`
* `PropsAction(unsigned long interval, void (*function)())`
`function` is the callback executed at every `interval` milliseconds.
## Members
* `void reset(unsigned long interval)`
* `void disable()`
* `void enable()`
* `void check()`
* `bool tick()`
* `unsigned long getInterval()`
## Usage
### Create a callback
```csharp
void clignote()
{
if (clignoter.value()) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
led.setValue(digitalRead(LED_BUILTIN));
}
}
```
### Create a *PropsAction* instance
```csharp
PropsAction clignoteAction = PropsAction(1000, clignote); // interval every 1000 milliseconds
```
### Call the instance `check()` method ine the sketch `loop()`
```csharp
void loop()
{
...
clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls
...
}
```
## Author
**Marie FAURE** (Oct 18th, 2019)
* company: FAURE SYSTEMS SAS
* mail: *dev at faure dot systems*
* github: <a href="https://github.com/fauresystems?tab=repositories" target="_blank">fauresystems</a>
* web: <a href="https://www.live-escape.net/" target="_blank">Live Escape Grenoble</a>
+76
View File
@@ -0,0 +1,76 @@
# *PropsData* reference
*PropsData* tracks data changes and provides escape room 2.0 propocol `DATA` ready string format.
See **4. Application protocol for escape room 2.0 props** in [README.md](../README.md).
*PropsData* is an interface (base class) implemented by 4 classes:
* ***PropsDataDecimal***
* ***PropsDataInteger***
* ***PropsDataLogical***
* ***PropsDataText***
## Constructors
* `PropsDataDecimal(const char *, uint8_t digits = 2, double precision = 0.1, double initial = 0)`
* `PropsDataInteger(const char *, double precision = 0.1, long initial = 0)`
* `PropsDataLogical(const char *, const char *trueval = NULL, const char *falseval = NULL, bool initial = false)`
* `PropsDataText(const char *)`
By default for *PropsDataLogical*, `trueval` is `"1"` band `falseval` is `"0"`.
For *PropsDataDecimal*, `digits` is the number of decimal digits in the display string.
For *PropsDataDecimal* and *PropsDataInteger*, `precision` is the percentage threshold for deciding whether a change has occurred or not.
## Interface
* `String fetch()`
* `String fetchChange()`
*fetch()* always returns the `"var=value "` string ready to be sent in a `DATA` protocol message and update the `_previous` value.
*fetchChange()* checks whether a change has occurred or not. If a change occured, returns the `"var=value "` string ready to be sent in a `DATA` protocol message and update the `_previous` value. If no change occured, returns and empty string.
## Accessors and Mutators
* `void setValue(const double|long|bool|String)`
* `double|long|bool|String value() const`
Accessors and Mutators are typed according to *PropsData* type.
## Usage
### Create a *PropsData* instance
```csharp
PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true);
```
### Add the *PropsData* instance to managed data of the *Props*
```csharp
void setup()
{
...
Props::addManagedLogicalData(&clignoter);
...
}
```
### Use the *PropsData* instance anywhere
```csharp
clignoter.setValue(true);
...
if (clignoter.value()) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
led.setValue(digitalRead(LED_BUILTIN));
}
```
## Author
**Marie FAURE** (Oct 18th, 2019)
* company: FAURE SYSTEMS SAS
* mail: *dev at faure dot systems*
* github: <a href="https://github.com/fauresystems?tab=repositories" target="_blank">fauresystems</a>
* web: <a href="https://www.live-escape.net/" target="_blank">Live Escape Grenoble</a>
+58
View File
@@ -0,0 +1,58 @@
# *WiFiNINA* firmware update
Nowadays, fall 2019, we still receive *WiFiNINA* arduinos with firmware 1.0.0.0 version (Arduino Uno WiFi Rev 2, Arduino NANO 33 IoT, Arduino MKR WiFi 1010, Arduino MKR VIDOR 4000).
***It is highly recommended to update *WiFiNINA* firmware to latest version.***
## Known issue with 1.0.0.0 version
`WiFi.config()` does not work, we can't set static IP address.
That's a very good reason to update the firmware.
## Update *WiFiNINA* firmware how-to
1. Upload *FirmwareUpdater* sketch
2. Open **WiFi101 / WiFiNINA Firmware Updater**
3. Test board connection
4. Update *WiFiNINA* firmware
### 1. Upload *FirmwareUpdater* sketch
Load the *FirmwareUpdater* sketch from **File > Examples > WiFiNINA > Tools > FirmwareUpdater**
![](wifinina-firmware-1.png)
### 2. Open *WiFi101 / WiFiNINA Firmware Updater*
Open the firmware updater from **Tools > WiFi101 / WiFiNINA Firmware Updater**
![](wifinina-firmware-2.png)
### 3. Test board connection
Select the board port:
![](wifinina-firmware-3.png)
And click **Test connection**:
![](wifinina-firmware-4.png)
### 4. Update *WiFiNINA* firmware
Select the latest firmware available and click **Update Firmware**:
![](wifinina-firmware-5.png)
Update process will take about a minute then you're done:
![](wifinina-firmware-6.png)
## Author
**Marie FAURE** (Nov 13th, 2019)
* company: FAURE SYSTEMS SAS
* mail: *dev at faure dot systems*
* github: <a href="https://github.com/fauresystems?tab=repositories" target="_blank">fauresystems</a>
* web: <a href="https://www.live-escape.net/" target="_blank">Live Escape Grenoble</a>
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB