Arduino with WiFi Shield make weird post request - post

I am working on small weather station based on Arduino Uno. In fact I am already create prototype which measure humidity, temperature, pressure and level of CO2 and send data trough POST request to server. For the whole week it works perfectly sending data to server on hourly basis. But yesterday I find out that no new data coming. My first thought was that something wrong with WiFi, I restart router, check connectivity, everything work perfect. I think if something wrong with Arduino and restart it, it works. So I check what I get after connection and answer was:
HTTP/1.1 405 METHOD NOT ALLOWED
Date: Fri, 02 Sep 2016 13:27:02 GMT
Server: Apache/2.4.10 (Debian)
Allow: GET, OPTIONS, POST, HEAD
Content-Length: 178
Connection: close
Content-Type: text/html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
*CLOS*
Ok then I send POST request to server manually (trough Postman) and it is works. So I go to server and start read logs, there is no errors but in access.log I find out something interesting:
Working post request coming from Postman look like:
15.15.119.103 - - [02/Sep/2016:13:54:03 +0300] "POST /api/meteo HTTP/1.1" 200 319 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"
But when it comes from Arduino it look in strange way
15.15.119.103 - - [02/Sep/2016:13:53:54 +0300] "*HELLO*POST /api/meteo HTTP/1.1" 405 380 "-" "-"
So as you can see it comes to server not like POST but LIKE "HELLOPOST" and it is ruined everything. The problem is that I change nothing in my code and it is working somehow during the week. You can see peace of my Arduino code bellow:
#include <WiFly.h>
#include "HTTPClient.h"
#define SSID "bbbbbbb"
#define KEY "ccccccc"
#define AUTH WIFLY_AUTH_WPA2_PSK
#define HTTP_POST_URL "15.15.25.67/api/meteo"
SoftwareSerial uart(2, 3);
WiFly wifly(uart);
HTTPClient http;
String PostData;
char PostBuf[90];
uart.begin(9600);
// check if WiFly is associated with AP(SSID)
if (!wifly.isAssociated(SSID)) {
while (!wifly.join(SSID, KEY, AUTH)) {
Serial.println("Failed to join " SSID);
Serial.println("Wait 0.1 second and try again...");
delay(100);
}
wifly.save(); // save configuration,
}
PostData.toCharArray(PostBuf, 90);
while (http.post(HTTP_POST_URL, PostBuf, 10000) < 0) {
}
while (wifly.receive((uint8_t *)&get, 1, 1000) == 1) {
Serial.print(get);
}
uart.end();
So it connect to WiFI and send request, but type of request is quite strange. I try to find any key which can help with no results maybe somebody can give me advise?
In case if needed I put here HTTPClient.h:
#ifndef __HTTP_CLIENT_H__
#define __HTTP_CLIENT_H__
#define HTTP_CLIENT_DEFAULT_TIMEOUT 30000 // 3s
#define HTTP_MAX_HOST_LEN 20
#define HTTP_MAX_PATH_LEN 64
#define HTTP_MAX_BUF_LEN 100
#define HTTP_DEFAULT_PORT 80
#include <Arduino.h>
#include <WiFly.h>
class HTTPClient {
public:
HTTPClient();
int get(const char *url, int timeout = HTTP_CLIENT_DEFAULT_TIMEOUT);
int get(const char *url, const char *header, int timeout = HTTP_CLIENT_DEFAULT_TIMEOUT);
int post(const char *url, const char *data, int timeout = HTTP_CLIENT_DEFAULT_TIMEOUT);
int post(const char *url, const char *headers, const char *data, int timeout = HTTP_CLIENT_DEFAULT_TIMEOUT);
private:
int parseURL(const char *url, char *host, int max_host_len, uint16_t *port, char *path, int max_path_len);
int connect(const char *url, const char *method, const char *data, int timeout = HTTP_CLIENT_DEFAULT_TIMEOUT);
int connect(const char *url, const char *method, const char *header, const char *data, int timeout = HTTP_CLIENT_DEFAULT_TIMEOUT);
WiFly* wifly;
};
#endif // __HTTP_CLIENT_H__
As for HTTPClient.cpp it looks like this:
#include <string.h>
#include "HTTPClient.h"
#include "Debug.h"
HTTPClient::HTTPClient()
{
wifly = WiFly::getInstance();
}
int HTTPClient::get(const char *url, int timeout)
{
return connect(url, "GET", NULL, NULL, timeout);
}
int HTTPClient::get(const char *url, const char *headers, int timeout)
{
return connect(url, "GET", headers, NULL, timeout);
}
int HTTPClient::post(const char *url, const char *data, int timeout)
{
return connect(url, "POST", NULL, data, timeout);
}
int HTTPClient::post(const char *url, const char *headers, const char *data, int timeout)
{
return connect(url, "POST", headers, data, timeout);
}
int HTTPClient::connect(const char *url, const char *method, const char *data, int timeout)
{
return connect(url, method, NULL, data, timeout);
}
int HTTPClient::connect(const char *url, const char *method, const char *headers, const char *data, int timeout)
{
char host[HTTP_MAX_HOST_LEN];
uint16_t port;
char path[HTTP_MAX_PATH_LEN];
if (parseURL(url, host, sizeof(host), &port, path, sizeof(path)) != 0) {
DBG("Failed to parse URL.\r\n");
return -1;
}
if (!wifly->connect(host, port, timeout)) {
DBG("Failed to connect.\r\n");
return -2;
}
// Send request
char buf[HTTP_MAX_BUF_LEN];
snprintf(buf, sizeof(buf), "%s %s HTTP/1.1\r\n", method, path);
wifly->send(buf);
// Send all headers
snprintf(buf, sizeof(buf), "Host: %s\r\nConnection: close\r\n", host);
wifly->send(buf);
if (data != NULL) {
snprintf(buf, sizeof(buf), "Content-Length: %d\r\nContent-Type: text/plain\r\n", strlen(data));
wifly->send(buf);
}
if (headers != NULL) {
wifly->send(headers);
}
// Close headers
wifly->send("\r\n");
// Send body
if (data != NULL) {
wifly->send(data);
}
return 0;
}
int HTTPClient::parseURL(const char *url, char *host, int max_host_len, uint16_t *port, char *path, int max_path_len)
{
char *scheme_ptr = (char *)url;
char *host_ptr = (char *)strstr(url, "://");
if (host_ptr != NULL) {
if (strncmp(scheme_ptr, "http://", 7)) {
DBG("Bad scheme\r\n");
return -1;
}
host_ptr += 3;
} else {
host_ptr = (char *)url;
}
int host_len = 0;
char *port_ptr = strchr(host_ptr, ':');
if (port_ptr != NULL) {
host_len = port_ptr - host_ptr;
port_ptr++;
if (sscanf(port_ptr, "%hu", port) != 1) {
DBG("Could not find port.\r\n");
return -3;
}
} else {
*port = HTTP_DEFAULT_PORT;
}
char *path_ptr = strchr(host_ptr, '/');
if (host_len == 0) {
host_len = path_ptr - host_ptr;
}
if (max_host_len < (host_len + 1)) {
DBG("Host buffer is too small.\r\n");
return -4;
}
memcpy(host, host_ptr, host_len);
host[host_len] = '\0';
int path_len;
char *fragment_ptr = strchr(host_ptr, '#');
if (fragment_ptr != NULL) {
path_len = fragment_ptr - path_ptr;
} else {
path_len = strlen(path_ptr);
}
if (max_path_len < (path_len + 1)) {
DBG("Path buffer is too small.\r\n");
return -5;
}
memcpy(path, path_ptr, path_len);
path[path_len] = '\0';
return 0;
}

I find out a root of problem, by default WiFiShield v.1.0 say "HELLO" when TCP connection opened. In fact it written deep into the manual.
My connection was not so fast, so it is manage to say "HELLO" before connectivity, but I upgrade router firmware and it start working faster, that is why "HELLO" connected to next request which was POST in this case. Solution is simple just add:
wifly.sendCommand("set comm remote 0\r");
and this command disable welcome message on WiFiShield. Hope it helps somebody.

Related

libmosquittopp - loop_start() returned error code 10 (0xA)

I'm trying to create my MQTT client(pub/sub) with "MQTTWrapper" class.
I tested connect_async() and loop_start() functions.
But loop_start() returned error code 10. I think it means "MOSQ_ERR_NOT_SUPPORTED".
I'm testing MQTT version 2.0.7.
Anyone who can help me?
mqtt_client.h
class mqtt_client : public mosqpp::mosquittopp
{
public:
mqtt_client (const char* id, const char* topic, const char* host, int port, int keepalive, int qos);
~mqtt_client();
void on_connect(int rc);
void on_disconnect(int rc);
int afx_connect();
int afx_disconnect();
int afx_publish(const char* msg);
};
mqtt_client.cpp
int mqtt_client::afx_connect()
{
int rc = connect_async(host_, port_, keepalive_);
if (rc != MOSQ_ERR_SUCCESS) {
CLog::WriteLog(_T("connect_async() failed. Code %d"), rc);
return rc;
}
//Start a thread and call mosquitto_loop() continuously in the thread to process network information
rc = loop_start();
if (rc != MOSQ_ERR_SUCCESS) {
CLog::WriteLog(_T("loop_start() failed. Code %d"), rc);
return rc;
}
return MOSQ_ERR_SUCCESS;
}

How to get CNAME of a domain in iOS

I would like to resolve a domain and get its CNAME and ip info, like the following:
example.com -> cname.com -> ip address
I searched a lot and know how to get the ip address, e.g. by CFHost. But I still don't know how to get other information, like its CNAME.
Should I do this with CFHost or something else?
You can use below code for your purpose
+ (void) getCNAME {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
DNSServiceRef serviceRef;
DNSServiceErrorType error;
error = DNSServiceQueryRecord(&serviceRef, 0, 0, "apple.com", kDNSServiceType_CNAME,
kDNSServiceClass_IN, queryCallback, NULL);
if (error != kDNSServiceErr_NoError){
NSLog(#"DNS Service error");
}
DNSServiceProcessResult(serviceRef);
DNSServiceRefDeallocate(serviceRef);
});}
static void queryCallback(DNSServiceRef sdRef,
DNSServiceFlags flags,
uint32_t interfaceIndex,
DNSServiceErrorType errorCode,
const char *fullname,
uint16_t rrtype,
uint16_t rrclass,
uint16_t rdlen,
const void *rdata,
uint32_t ttl,
void *context) {
if (errorCode == kDNSServiceErr_NoError && rdlen > 1) {
NSMutableData *txtData = [NSMutableData dataWithCapacity:rdlen];
for (uint16_t i = 1; i < rdlen; i += 256) {
[txtData appendBytes:rdata + i length:MIN(rdlen - i, 255)];
}
NSString *theTXT = [[NSString alloc] initWithBytes:txtData.bytes length:txtData.length encoding:NSASCIIStringEncoding];
NSLog(#"CNAME: %#", theTXT);
}}

Can I poll for file to run another code?

Client side:
#define BUFFSIZE 4096
main(argc, argv)
int argc;
char *argv[];
{
int fd,i,n;
char buff[BUFFSIZE];
extern char *pname;
pname = argv[0];
argv++; argc--;
fd=0;
i=0;
do{
if(arg>0 && (fd=my_open(argv[i],0)) <0) {
err_ret("cant open %s", argv[i]);
continue;
}
while((n=read(fd,buff,BUFFSIZE))>0)
if (write(1,buff,n) !=n)
err_sys("write error:);
if(n<0)
err_sys("read error");
} while(++i<argc);
exit(0);
}
Server side:
main(argc, argv)
int argc;
char *intv[];
{
int fd;
extern int errno;
extern char *pname;
pname= argv[0];
if(argc !=4)
err_quit("open file <sockfd#> <filename><mode>");
if((fd=open(argv[2],atoi(argv[3]))) < 0)
exit((errno>0) ? errno:255);
exit(sendfile(atoi(argv[1]),fd));
}
If I send a config file from client to server like this, then can I run a polling code on server which runs a python code on receiving config file.

Query DNS to find NAPTR in iOS

I have been having a lot of trouble finding a way to query the DNS to find the NAPTR in iOS. There seem to be many relatively simple ways to resolve to an IP, but I specifically need to find all NAPTR records in a DNS lookup. I'd prefer to do so without having to bring in any external libraries if at all possible. If anyone has been able to do this (or something similar that I can extrapolate from) I'd appreciate any pointers.
All code must function in iOS 5.0+
I ended up using DNSServiceQueryRecord.
DNSServiceRef sdRef;
DNSServiceQueryRecord(&sdRef, 0, 0,
"google.com",
kDNSServiceType_NAPTR,
kDNSServiceClass_IN,
callback,
NULL);
DNSServiceProcessResult(sdRef);
DNSServiceRefDeallocate(sdRef);
In actual use, I found that there was an issue where the app would hang indefinitely if there were no results, so I ended up having to adjust my code to add a timeout on the result.
/*
Attempt to fetch the NAPTR from the stored server address. Since iOS will continue waiting
until told directly to stop (even if there is no result) we must set our own timeout on the
request (set to 5 seconds).
On success, the callback function is called. On timeout, the kSRVLookupComplete notification
is sent.
*/
- (void)attemptNAPTRFetch {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
DNSServiceRef sdRef;
DNSServiceErrorType err;
err = DNSServiceQueryRecord(&sdRef, 0, 0,
[server cStringUsingEncoding:[NSString defaultCStringEncoding]],
kDNSServiceType_NAPTR,
kDNSServiceClass_IN,
callback,
NULL);
// This stuff is necessary so we don't hang forever if there are no results
int dns_sd_fd = DNSServiceRefSockFD(sdRef);
int nfds = dns_sd_fd + 1;
fd_set readfds;
struct timeval tv;
int result;
int stopNow = 0;
int timeOut = 5; // Timeout in seconds
while (!stopNow) {
FD_ZERO(&readfds);
FD_SET(dns_sd_fd, &readfds);
tv.tv_sec = timeOut;
tv.tv_usec = 0;
result = select(nfds, &readfds, (fd_set*)NULL, (fd_set*)NULL, &tv);
if (result > 0) {
if(FD_ISSET(dns_sd_fd, &readfds)) {
err = DNSServiceProcessResult(sdRef);
if (err != kDNSServiceErr_NoError){
NSLog(#"There was an error");
}
stopNow = 1;
}
}
else {
printf("select() returned %d errno %d %s\n", result, errno, strerror(errno));
if (errno != EINTR) {
stopNow = 1;
postNotification(kSRVLookupComplete, nil);
}
}
}
DNSServiceRefDeallocate(sdRef);
});
}
Then, for the callback:
static void callback(DNSServiceRef sdRef,
DNSServiceFlags flags,
uint32_t interfaceIndex,
DNSServiceErrorType errorCode,
const char *fullname,
uint16_t rrtype,
uint16_t rrclass,
uint16_t rdlen,
const void *rdata,
uint32_t ttl,
void *context)
{
uint16_t order, pref;
char flag;
NSMutableString *service = [[NSMutableString alloc] init];
NSMutableString *replacement = [[NSMutableString alloc] init];
const char *data = (const char*)rdata;
order = data[1];
pref = data[3];
flag = data[5];
int i = 7;
while (data[i] != 0){
[service appendString:[NSString stringWithFormat:#"%c", data[i]]];
i++;
}
i += 2;
while(data[i] != 0){
if(data[i] >= 32 && data[i] <= 127)
[replacement appendString:[NSString stringWithFormat:#"%c", data[i]]];
else
[replacement appendString:#"."];
i++;
}
NSLog(#"\nOrder: %i\nPreference: %i\nFlag: %c\nService: %#\nReplacement: %#\n", order, pref, flag, service, replacement);
}
This seems to do the trick for me. You would of course do any other necessary work using all the parsed data in the callback or store the data somewhere to be used later.

A question of libevent example code: how is invoked?

I'm learning libev however the code is so hard to understand, so I choose to learn libevent first whose code is relatively clearer. But I encounter a problem when try the example (http://www.wangafu.net/~nickm/libevent-book/01_intro.html).
How is the code event_add(state->write_event, NULL) in do_read() make do_write() function invoked?
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For fcntl */
#include <fcntl.h>
#include <event2/event.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#define MAX_LINE 16384
void do_read(evutil_socket_t fd, short events, void *arg);
void do_write(evutil_socket_t fd, short events, void *arg);
char
rot13_char(char c)
{
return c;
/* We don't want to use isalpha here; setting the locale would change
* which characters are considered alphabetical. */
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
struct fd_state {
char buffer[MAX_LINE];
size_t buffer_used;
size_t n_written;
size_t write_upto;
struct event *read_event;
struct event *write_event;
};
struct fd_state *
alloc_fd_state(struct event_base *base, evutil_socket_t fd)
{
struct fd_state *state = malloc(sizeof(struct fd_state));
if (!state)
return NULL;
state->read_event = event_new(base, fd, EV_READ|EV_PERSIST, do_read, state);
if (!state->read_event) {
free(state);
return NULL;
}
state->write_event =
event_new(base, fd, EV_WRITE|EV_PERSIST, do_write, state);
if (!state->write_event) {
event_free(state->read_event);
free(state);
return NULL;
}
state->buffer_used = state->n_written = state->write_upto = 0;
assert(state->write_event);
return state;
}
void
free_fd_state(struct fd_state *state)
{
event_free(state->read_event);
event_free(state->write_event);
free(state);
}
void
do_read(evutil_socket_t fd, short events, void *arg)
{
struct fd_state *state = arg;
char buf[1024];
int i;
ssize_t result;
while (1) {
assert(state->write_event);
result = recv(fd, buf, sizeof(buf), 0);
if (result <= 0)
break;
for (i=0; i < result; ++i) {
if (state->buffer_used < sizeof(state->buffer))
state->buffer[state->buffer_used++] = rot13_char(buf[i]);
if (buf[i] == '\n') {
assert(state->write_event);
**event_add(state->write_event, NULL);**
state->write_upto = state->buffer_used;
}
}
}
if (result == 0) {
free_fd_state(state);
} else if (result < 0) {
if (errno == EAGAIN) // XXXX use evutil macro
return;
perror("recv");
free_fd_state(state);
}
}
void
**do_write(evutil_socket_t fd, short events, void *arg)**
{
struct fd_state *state = arg;
while (state->n_written < state->write_upto) {
ssize_t result = send(fd, state->buffer + state->n_written,
state->write_upto - state->n_written, 0);
if (result < 0) {
if (errno == EAGAIN) // XXX use evutil macro
return;
free_fd_state(state);
return;
}
assert(result != 0);
state->n_written += result;
}
if (state->n_written == state->buffer_used)
state->n_written = state->write_upto = state->buffer_used = 1;
event_del(state->write_event);
}
void
do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = arg;
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listener, (struct sockaddr*)&ss, &slen);
if (fd < 0) { // XXXX eagain??
perror("accept");
} else if (fd > FD_SETSIZE) {
close(fd); // XXX replace all closes with EVUTIL_CLOSESOCKET */
} else {
struct fd_state *state;
evutil_make_socket_nonblocking(fd);
state = alloc_fd_state(base, fd);
assert(state); /*XXX err*/
assert(state->write_event);
event_add(state->read_event, NULL);
}
}
void
run(void)
{
evutil_socket_t listener;
struct sockaddr_in sin;
struct event_base *base;
struct event *listener_event;
base = event_base_new();
if (!base)
return; /*XXXerr*/
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(40713);
listener = socket(AF_INET, SOCK_STREAM, 0);
evutil_make_socket_nonblocking(listener);
#ifndef WIN32
{
int one = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
}
#endif
if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
perror("bind");
return;
}
if (listen(listener, 16)<0) {
perror("listen");
return;
}
listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
/*XXX check it */
event_add(listener_event, NULL);
event_base_dispatch(base);
}
int
main(int c, char **v)
{
setvbuf(stdout, NULL, _IONBF, 0);
run();
return 0;
}
I'm not sure if I'm answering the same question you asked - I understand it as:
How does calling event_add(state->write_event, NULL) in do_read() lead to do_write() being invoked?
The key to figuring this out is understanding what the do_read() function is actually doing. do_read() is a callback function associated with a socket which has data to be read: this is set up with allocate_fd_state():
struct fd_state *
alloc_fd_state(struct event_base *base, evutil_socket_t fd)
{
/*
* Allocate a new fd_state structure, which will hold our read and write events
* /
struct fd_state *state = malloc(sizeof(struct fd_state));
[...]
/*
* Initialize a read event on the given file descriptor: associate the event with
* the given base, and set up the do_read callback to be invoked whenever
* data is available to be read on the file descriptor.
* /
state->read_event = event_new(base, fd, EV_READ|EV_PERSIST, do_read, state);
[...]
/*
* Set up another event on the same file descriptor and base, which invoked the
* do_write callback anytime the file descriptor is ready to be written to.
*/
state->write_event =
event_new(base, fd, EV_WRITE|EV_PERSIST, do_write, state);
[...]
return state;
}
At this point, though, neither of these events have been event_add()'ed to the event_base base. The instructions for what to do are all written out, but no one is looking at them. So how does anything get read? state->read_event is event_add()'ed to the base after an incoming connection is made. Look at do_accept():
void
do_accept(evutil_socket_t listener, short event, void *arg)
{
[ ... accept a new connection and give it a file descriptor fd ... ]
/*
* If the file descriptor is invalid, close it.
*/
if (fd < 0) { // XXXX eagain??
perror("accept");
} else if (fd > FD_SETSIZE) {
close(fd); // XXX replace all closes with EVUTIL_CLOSESOCKET */
/*
* Otherwise, if the connection was successfully accepted...
*/
} else {
[ ... allocate a new fd_state structure, and make the file descriptor non-blocking ...]
/*
* Here's where the magic happens. The read_event created back in alloc_fd_state()
* is finally added to the base associated with it.
*/
event_add(state->read_event, NULL);
}
}
So right after accepting a new connection, the program tells libevent to wait until there's data available on the connection, and then run the do_read() callback. At this point, it's still impossible for do_write() to be called. It needs to be event_add()'ed. This happens in do_read():
void
do_read(evutil_socket_t fd, short events, void *arg)
{
/* Create a temporary buffer to receive some data */
char buf[1024];
while (1) {
[ ... Receive the data, copying it into buf ... ]
[ ... if there is no more data to receive, or there was an error, exit this loop... ]
[ ... else, result = number of bytes received ... ]
for (i=0; i < result; ++i) {
[ ... if there's room in the buffer, copy in the rot13() encoded
version of the received data ... ]
/*
* Boom, headshot. If we've reached the end of the incoming data
* (assumed to be a newline), then ...
*/
if (buf[i] == '\n') {
[...]
/*
* Have libevent start monitoring the write_event, which calls do_write
* as soon as the file descriptor is ready to be written to.
*/
event_add(state->write_event, NULL);
[...]
}
}
}
[...]
}
So, after reading in some data from a file descriptor, the program starts waiting until
the file descriptor is ready to be written to, and then invokes do_write(). Program
flow looks like this:
[ set up an event_base and start waiting for events ]
[ if someone tries to connect ]
[ accept the connection ]
[ ... wait until there is data to read on the connection ... ]
[ read in data from the connection until there is no more left ]
[ ....wait until the connection is ready to be written to ... ]
[ write out our rot13() encoded response ]
I hope that a) that was the correct interpretation of your question, and b) this was a helpful answer.

Resources