diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b8bd81..f11a875 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: build-type: [ debug, release ] - demo: [compartmentalisation, configuration_broker_ibex, configuration_broker_sonata, HughV1, HughV2] + demo: [compartmentalisation, configuration_broker_ibex, configuration_broker_sonata, HughV1, HughV2, smart_meter] include: - xmake-run: false - build-type: debug @@ -39,6 +39,9 @@ jobs: - demo: configuration_broker_ibex build-dir: configuration_broker/ibex-safe-simulator xmake-run: true + - demo: smart_meter + build-dir: smartmeter/cheriot + extra-config: --IPv6=n fail-fast: false runs-on: ubuntu-latest container: diff --git a/cheriot-rtos b/cheriot-rtos index 4cf1e5b..7867118 160000 --- a/cheriot-rtos +++ b/cheriot-rtos @@ -1 +1 @@ -Subproject commit 4cf1e5becaea1d8421f88e7e45a0e08cd65ee2f6 +Subproject commit 78671186d79c1c7046fdb8d03f4bbf1c614cf380 diff --git a/network-stack b/network-stack index 8a05cc1..a5d307f 160000 --- a/network-stack +++ b/network-stack @@ -1 +1 @@ -Subproject commit 8a05cc1cdc1d97d2a05c5b2b1ddd45a7e3479047 +Subproject commit a5d307f3e187194d15eccf47edd03fd4d02438ac diff --git a/smartmeter/cheriot/README.house-protocol.md b/smartmeter/cheriot/README.house-protocol.md new file mode 100644 index 0000000..eeba4a8 --- /dev/null +++ b/smartmeter/cheriot/README.house-protocol.md @@ -0,0 +1,63 @@ +# What + +This is a sketch of a protocol between the CHERIoT smartmeter demo components and +and external device representing the house's power distribution system. + +This is based on Murali's proposal at +https://github.com/CHERIoT-Platform/cheriot-demos/pull/20#issuecomment-2670632663 + +# Messages + +For ease of debugging, this protocol uses ASCII. +Messages are delimited with newlines (`0x0a`). +Messages are composed of space (`0x20`) separated tokens. +The tokens within a message specify a command (possibly with subcommands) and then its arguments. + +## Smartmeter to House + +(In the smartmeter, these messages are to be sent by the `user` compartment.) + +### Set diagnostic LEDs + + led [on|off] [n] + +Change diagnostic LEDs on the house side. +The optional `n` field is a bitmask of LEDs to be turned on or off; +if unspecified, an unspecified default is used. + +### Control battery charge / discharge + + batteryControl N POWER + +Indicates that battery N should be charged at a given POWER (in Watts). +If RATE is negative, the battery should be discharged. +The house should respond with a battery status message. + +### Power draw + + powerTarget N POWER + +Indicates the target POWER draw (in Watts) for consumer N. + +## House to Smartmeter + +(In the smartmeter, these messages are to be processed by the `sensor` compartment.) + +### Battery status + + batteryStatus N CAPACITY CHARGE + +Indicates that battery N has total CAPACITY (in Watt-hours) and is currently holding CHARGE (also Watt-hours). + +This message should be sent every minute for each battery before its `powerSample` report, and +it should also be sent in response to `batteryControl` message. + +### Power draw + + powerSample POWER + +A report of total POWER (in Watts) draw for the past minute. +In a real system, this would be measured directly by the smartmeter. +Negative POWER values indicate that the house is feeding back to the grid. + +This message should be sent every minute after all `batteryStatus` reports. diff --git a/smartmeter/cheriot/README.md b/smartmeter/cheriot/README.md new file mode 100644 index 0000000..a8aeae8 --- /dev/null +++ b/smartmeter/cheriot/README.md @@ -0,0 +1,188 @@ +# Disclaimer + +This is a work-in-progress and is only ever intended as a technology demonstrator. +(That is, while hopefully interesting, this code should not find its way to production environments!) + +# Multi-Tenant Smart Metering + +This demo showcases how one might run multiple tenants with different operational concerns on a sensing platform. +Specifically, we consider the task of "smart metering" power consumption. +Our tenants are... + +1. The grid controller, who wants to + + 1. receive real-time information about the local grid, + 2. communicate scheduled outages, + 3. indicate the need for corrective actions (load shedding, load increase) + +2. The provider, who wants to + + 1. receive usage data (even retrospectively, in the case of, say, network outages), + 2. set the price schedule ahead of time, and + 3. announce spot price variances relative to the schedule + +3. The service integrator (or, perhaps, end user), who wants to + + 1. receive the price schedule, variances, and grid notifications, and + 2. make policy decisions about local resources (batteries, generation, loads) based on those. + +We presume that the grid controller and provider logic is minimal and can be fixed at firmware build time, +while the integrator/user policy may be subject to faster change and should not require rebuilding firmware. +To that end, that policy is run on a lightweight JS interpreter. + +## Compartmentalization and Information Flow + +Internally, these tenants all exist within their own compartments ("grid", "provider", and two for the "user"). +These make use of the (also compartmentalized!) MQTT/TLS/TCP/IP network stack to talk to the network. + +Sensing and measurement itself is done by another compartment, "sensor". +This compartment makes its measurements available via a static shared object. +This object contains a small ring buffer of recent measurements and a time-stamp of the most recent. +(We presume the tenant compartments are sufficiently responsive that they will not miss updates.) + +Similarly, information from the grid controller and providers are also exposed via shared objects. + +These shared objects all contain futex words used as version identifiers and update-in-progress flags, +so it is possible to get stable reads of their contents and to wait for updates. + +## Building The CHERIoT Bits + +The build proceeds via the familiar CHERIoT-RTOS `xmake` system. + +### Overriding Source Dependencies + +By default, the build will use the RTOS and network stack submodules of the containing repository. +You can use a different RTOS source tree by setting the ``CHERIOT_RTOS_SDK`` environment variable. +Similarly, a different network stack source tree can be called for with ``CHERIOT_NETWORK_STACK``. + +### Non-Default MQTT Broker + +By default, the device will use `test.mosquitto.org` as the broker, and +the build expects the file named `mosquitto.org.h` to hold the appropriate TLS X.509 trust anchors. +These, respectively, can be overridden with the `--broker-host` and `--broker-anchor` options to `xmake config`. + +### Static Device Identifier + +Left to its own devices, the demo will generate a random 8-character identifier for itself. +This can instead be baked into the firmware by passing `--unique-id` to `xmake config`. +The value must be exactly 8 ASCII alphanumeric characters (`[A-Za-z0-9]`). + +## Running + +Most MQTT messages are composed of space-separated integers, +and so are generally straightforward to synthesize by hand on the command line or with other tooling. + +At startup, the device will say something like + + housekeeping: MQTT Unique: S8MhTd2T + +This 8-character sequence is used to distinguish multiple devices connected to the broker. +In practice, some of these topics (provider and grid) would be broadcast, with many devices subscribing. +For the purpose of this demo, all topics are "uniquified". +The examples below assume that this string is in the environment variable `METER_ID`. + +### Subscribing to MQTT updates + +The device publishes to `cheriot-smartmeter/p/update/${METER_ID}` (provider compartment) +and `cheriot-smartmeter/g/update/${METER_ID}` (grid compartment). + +### Setting the provider rate schedule + +The provider rate schedule contains a timestamp and 48 values thereafter. +Run something like the following to denote a time-of-use schedule for today +(that is, starting at the most recent past midnight) +and a flat rate schedule for tomorrow. + +The units here are intended to be centi-pence per kWh, which should give a realistic amount of precision. + + mosquitto_pub -h test.mosquitto.org -p 1883 -q 1 -t cheriot-smartmeter/p/schedule/${METER_ID} -s << HERE + $(date +%s --date="") + 760 760 760 760 760 760 760 760 1580 1580 1580 1220 1220 1220 1220 1580 1580 1580 760 760 760 760 760 760 + 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 760 + HERE + +### Setting a provider variance + +To inform the device of a variance in pricing (say, a surplus of solar power is driving prices negative), +use something like the following, which indicates two variances: +- in the first, starting now and lasting for 5 minutes, power has negative cost +- in the second, starting in 5 minutes and lasting a further 10, power has zero cost + +Run: + + mosquitto_pub -h test.mosquitto.org -p 1883 -q 1 -t cheriot-smartmeter/p/variance/${METER_ID} -s << HERE + $(date +%s) + 0 300 -2 + 300 600 0 + HERE + +### Scheduling a grid outage + +To inform the device of a pending loss of grid power, run something like the following, +which indicates an outage starting in 4 hours and lasting for 6 thereafter. + + mosquitto_pub -h test.mosquitto.org -p 1883 -q 1 -t cheriot-smartmeter/g/outage/${METER_ID} -s << HERE + $(($(date +%s) + 4*3600)) $((6*3600)) + HERE + +### Transmitting a grid request + +To inform the device of a request for more or less production, run something like the following, +which indicates an immediate request, lasting for 5 minutes, to reduce consumption (or increase generation). + + mosquitto_pub -h test.mosquitto.org -p 1883 -q 1 -t cheriot-smartmeter/g/request/${METER_ID} -s << HERE + $(date +%s) 300 -1024 + HERE + +### Pushing new JS Policy Code + +The `js/compile.sh` script wraps fetching `microvium` (and its Node.js backend, in particular) +and using that to build bytecode files for use with the device. +Running, for example, `compile.sh ./sample_policy.js`, will result in a `sample_policy.js.mvm-bc` file that can be shipped over MQTT to the device with a command like + + mosquitto_pub -h test.mosquitto.org -p 1883 -q 1 -t cheriot-smartmeter/u/js/${METER_ID} -s < sample_policy.mvm-bc + +### Speeding Up Simulated Time + +Because this is a _demonstration_ and not a _real device_, it's helpful to be able to run faster than real time. +Towards that end, the policy evaluator understands a "timebase zero", +after which (simulated) time elapses at a positive multiple rate relative to real time. +While the policy code will still be evaluated in real time +(and, in particular, in response to sensor data that will continue to be published every 30 _wall-clock_ seconds), +the timestamp associated sensor reports, which drives most of the policy's idea of time, +will be artificially advanced. +Specifically, given a _wall-clock_ time T, a timebase zero Z, and a timebase rate R, +if T >= Z, then the time will instead be indicated as + + Z + R * (T - Z) + +Thus, setting R to 10, for example, will cause the policy code's 30 _wall-clock_ second evaulation interval to +correspond to a 5 _simulated minute_ interval. + +As with most everything, setting the timebase is done over MQTT. Run something like + + mosquitto_pub -h test.mosquitto.org -p 1883 -q 1 -t cheriot-smartmeter/u/timebase/${METER_ID} -s << HERE + $(date +%s --date="12 minutes ago") 10 + HERE + +to set the timebase zero (Z) to 12 minutes ago and the rate (R) to 10. +(Making the timebase zero two _simulated hours_ ago.) + +You _may_ wish to have the broker retain this message, so that subsequent subscribers will also be informed. +To do so, add `-r` (or `--retain`) to the command line (before the `<<`). +MQTT message retention is entirely at the discretion of the server, so may not be reliable. +Don't abuse public resources. + +## Development + +### Changing the JS FFI + +If you change the information cached and exposed in the `userJS_snapshot` structure (also defined in `common.hh`), +you'll want to update the constants in `js/ffi.js`, tweak `js/ffigen.js`, and run `microvium ffigen.js` (in the `js/` directory) to rebuild `userffi.h`. + +### Updating the default JS + +At boot or after a crash, the device reloads an initial JS bytecode. +That's sourced from `default-javascript.h`, which can be generated by, in the `js` directory, + + ./compile.sh -H ../default_javascript.h ./sample_policy.js diff --git a/smartmeter/cheriot/cheriot.demo.h b/smartmeter/cheriot/cheriot.demo.h new file mode 100644 index 0000000..52b7bc0 --- /dev/null +++ b/smartmeter/cheriot/cheriot.demo.h @@ -0,0 +1,31 @@ + +static const unsigned char TA0_DN[] = { + 0x30, 0x17, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x63, 0x68, 0x65, 0x72, 0x69, 0x6F, 0x74, 0x2E, 0x64, 0x65, 0x6D, + 0x6F +}; + +static const unsigned char TA0_EC_Q[] = { + 0x04, 0xAD, 0x02, 0x25, 0x3D, 0x08, 0x4F, 0xE0, 0x61, 0x99, 0x67, 0xFF, + 0x97, 0x2E, 0xE8, 0x7E, 0x2C, 0x91, 0x17, 0xCB, 0x4F, 0xE7, 0xD4, 0x1D, + 0x7F, 0x6B, 0x38, 0x87, 0x3C, 0x2B, 0x95, 0xC1, 0xFC, 0xD9, 0x0C, 0x90, + 0x8A, 0x8D, 0x44, 0x28, 0x7D, 0x2F, 0x0F, 0x18, 0x6B, 0xE9, 0xDA, 0x20, + 0xDB, 0x9A, 0xDF, 0xA9, 0x33, 0x71, 0x12, 0xC4, 0x0C, 0x37, 0x11, 0xB6, + 0x12, 0x27, 0x52, 0xAE, 0x10 +}; + +static const br_x509_trust_anchor TAs[1] = { + { + { (unsigned char *)TA0_DN, sizeof TA0_DN }, + BR_X509_TA_CA, + { + BR_KEYTYPE_EC, + { .ec = { + BR_EC_secp256r1, + (unsigned char *)TA0_EC_Q, sizeof TA0_EC_Q, + } } + } + } +}; + +#define TAs_NUM 1 diff --git a/smartmeter/cheriot/common.hh b/smartmeter/cheriot/common.hh new file mode 100644 index 0000000..8d119de --- /dev/null +++ b/smartmeter/cheriot/common.hh @@ -0,0 +1,289 @@ +#pragma once +#include +#include +#include +#include + +/** + * @defgroup entryvectors Compartment entry vectors + * @{ + */ + +#ifdef MONOLITH_BUILD_WITHOUT_SECURITY +# define SMARTMETER_COMPARTMENT(x) __cheri_compartment("monolith") +# define SMARTMETER_USER_COMPARTMENT __cheri_compartment("monolithUser") +#else +# define SMARTMETER_COMPARTMENT(x) __cheri_compartment(x) +# define SMARTMETER_USER_COMPARTMENT __cheri_compartment("user") +#endif + +int SMARTMETER_COMPARTMENT("grid") grid_entry(); +int SMARTMETER_COMPARTMENT("housekeeping") housekeeping_entry(); +int SMARTMETER_COMPARTMENT("provider") provider_entry(); +int SMARTMETER_COMPARTMENT("sensor") sensor_entry(); +int SMARTMETER_USER_COMPARTMENT user_data_entry(); +int SMARTMETER_USER_COMPARTMENT user_net_entry(); + +/* @} */ + +/** + * @defgroup crosscalls Compartment cross-calls + * @{ + */ + +/** + * Wait for housekeeping to perform initialization tasks + */ +void SMARTMETER_COMPARTMENT("housekeeping") housekeeping_initial_barrier(); + +static constexpr size_t housekeeping_mqtt_unique_size = 8; + +/** + * The housekeeping compartment also builds a per-boot 8-character unique value + * for us for use with client IDs and subscription topics. We can ask it to + * copy it out to a local buffer. + */ +const char *SMARTMETER_COMPARTMENT("housekeeping") + housekeeping_mqtt_unique_get(); + +/** + * This pattern shows up in all our MQTT threads, so give it a short name. + */ +#define HOUSEKEEPING_MQTT_CONCAT(out, prefix, mqttUnique) \ + memcpy(out.data(), prefix.data(), prefix.size()); \ + memcpy( \ + out.data() + prefix.size(), mqttUnique, housekeeping_mqtt_unique_size); + +/** + * Replace the user policy code in the JS compartment + * + * Called by user + */ +int SMARTMETER_COMPARTMENT("userJS") + user_javascript_load(const uint8_t *bytecode, size_t size); + +/** + * Run user policy code + * + * Called by user + */ +int SMARTMETER_COMPARTMENT("userJS") user_javascript_run(/* XXX */); + +/* @} */ + +/** + * @defgroup sharedstate Shared object types + * + * Version fields will be treated as futexes and will be odd during updates. + * + * @{ + */ + +template +struct FutexVersioned +{ + /* + * If the version is congruent (mod 4) to ..., then payloads are ... : + * - 0, uninitialized; + * - 1, initialized and stable; or + * - 3, being updated. + */ + + uint32_t version; + Payload payload; + + void write(Payload &update) + { + // XXX: it's a pity we don't have C++20's atomic_ref + + __c11_atomic_thread_fence(__ATOMIC_RELEASE); + this->version |= 0x3; + this->payload = update; + __c11_atomic_thread_fence(__ATOMIC_RELEASE); + this->version += 2; + futex_wake(&this->version, UINT32_MAX); + } + + int read(Timeout *t, uint32_t &version, Payload &out) + { + uint32_t version_post; + + uint32_t version_pre = this->version; + + if (version_pre == version) + return -ENOMSG; + + do + { + while ((version_pre & 0x2) != 0) + { + int res = futex_timed_wait(t, &this->version, version_pre); + if (res < 0) + { + return res; + } + version_pre = this->version; + } + + out = this->payload; + __c11_atomic_thread_fence(__ATOMIC_ACQUIRE); + version_post = this->version; + + } while (version_pre != version_post); + + version = version_pre; + + return 0; + } +}; + +struct sensor_data_fine_payload +{ + uint32_t timestamp; // of samples[0], each next a minute back in the past + int32_t samples[8]; // The past few minutes of sensor data +}; + +using sensor_data_fine = FutexVersioned; +static_assert(sizeof(sensor_data_fine) == 40, + "sensor_data object bad size; update xmake.lua"); + +static constexpr size_t SENSOR_COARSENING = 300; + +struct sensor_data_coarse_payload +{ + uint32_t timestamp; // of samples[0], each SENSOR_COARSENING samples back + int32_t samples[6]; +}; + +using sensor_data_coarse = FutexVersioned; +static_assert(sizeof(sensor_data_coarse) == 32, + "sensor_data object bad size; update xmake.lua"); + +/** + * The next planned outage, if any. + * + * 0-duration outages do not exist. + */ +struct grid_planned_outage_payload +{ + uint32_t start_time; // Outage start time + uint32_t duration; // seconds after start +}; + +using grid_planned_outage = FutexVersioned; +static_assert(sizeof(grid_planned_outage) == 12, + "grid_planned_outage object bad size; update xmake.lua"); + +/** + * A grid request for load modulation. + * + * Sign of `severity` indicates direction of request: + * - push (negative; grid running low) or + * - pull (positive; grid has excess power) + * Magnitude is arbitrary but intended to reflect the risk of grid failure. + */ +struct grid_request_payload +{ + uint32_t start_time; + uint16_t duration; + int16_t severity; +}; + +using grid_request = FutexVersioned; +static_assert(sizeof(grid_request) == 12, + "grid_request object bad size; update xmake.lua"); + +/** + * Provider specified rate schedule + */ +struct provider_schedule_payload +{ + uint32_t timestamp_day; // Start of rate array; "today's midnight" + int16_t rate[48]; // Hourly rates, today then tomorrow, centipence/kWh +}; + +using provider_schedule = FutexVersioned; +static_assert(sizeof(provider_schedule) == 104, + "provider_schedule object bad size; update xmake.lua"); + +/** + * Provider-signaled variance in pricing. + * + * Up to two variances may be reported at once, so that we can report both + * the current one and an impending one. A zero duration variance does not + * exist. + */ +struct provider_variance_payload +{ + uint32_t timestamp_base; + + int16_t start[2]; // seconds relative to timestamp_update, negative past + uint16_t duration[2]; // seconds from start that variance applies + int16_t rate[2]; // Metering rate during this interval, p/kWh +}; + +using provider_variance = FutexVersioned; +static_assert(sizeof(provider_variance) == 20, + "provider_variance object bad size; update xmake.lua"); + +/** + * User signaled crash reporting + */ +struct user_crash_count_payload +{ + uint32_t crashes_since_boot; +}; + +using user_crash_count = FutexVersioned; +static_assert(sizeof(user_crash_count) == 8, + "user_crash_count object bad size; update xmake.lua"); + +/* + * The most recent stable snapshot of all the data sources we're + * monitoring. + * + * Updated by user compartment before entering userJS to respond. + */ +struct userjs_snapshot +{ + struct sensor_data_fine_payload sensor_data; + + struct grid_planned_outage_payload grid_outage; + struct grid_request_payload grid_request; + + struct provider_schedule_payload provider_schedule; + struct provider_variance_payload provider_variance; +}; +static_assert(sizeof(struct userjs_snapshot) == 168, + "userjs_snapshot object bad size; update xmake.lua"); + +/* @} */ + +/* + * In a compartmentalized build, we use static shared objects for communication. + * In a monolithic build, just use globals, as we would in a non-CHERI system. + */ +#ifdef MONOLITH_BUILD_WITHOUT_SECURITY +struct merged_data +{ + sensor_data_fine sensor_data_fine; + sensor_data_coarse sensor_data_coarse; + userjs_snapshot userjs_snapshot; +}; + +// in monolith compartment; exposed via getter to monolithUser +extern struct merged_data theData; + +struct merged_data *SMARTMETER_COMPARTMENT("monolith") + monolith_merged_data_get(); + +static_assert( + (offsetof(struct merged_data, userjs_snapshot.provider_schedule.rate) - + offsetof(struct merged_data, sensor_data_fine.payload.samples[0])) == 120, + "Offsets shifted; update attack demo SENSOR_OFFSET_COARSE"); + +static_assert( + (offsetof(struct merged_data, userjs_snapshot.provider_schedule.rate) - + offsetof(struct merged_data, sensor_data_coarse.payload.samples[0])) == 80, + "Offsets shifted; update attack demo SENSOR_OFFSET_FINE."); +#endif diff --git a/smartmeter/cheriot/default-javascript.h b/smartmeter/cheriot/default-javascript.h new file mode 100644 index 0000000..ec94fbd --- /dev/null +++ b/smartmeter/cheriot/default-javascript.h @@ -0,0 +1,172 @@ +/* + * Auto-generated file from sample_policy.js; do not edit. + * See js/compile.sh. + */ +unsigned char hello_mvm_bc[] = { + 0x08, 0x1c, 0x00, 0x00, 0x6e, 0x08, 0x40, 0x7f, 0x03, 0x00, 0x00, 0x00, 0x1c, + 0x00, 0x42, 0x00, 0x46, 0x00, 0x46, 0x00, 0x54, 0x00, 0xba, 0x00, 0xb0, 0x07, + 0xba, 0x07, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, + 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, + 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, + 0x00, 0xd2, 0x04, 0xcd, 0x06, 0xb9, 0x07, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x71, 0x04, 0x7d, 0x04, 0xd1, 0x00, 0xc9, + 0x00, 0x89, 0x02, 0x6d, 0x02, 0xc5, 0x02, 0xe5, 0x02, 0xa5, 0x02, 0x2d, 0x03, + 0x4d, 0x03, 0x05, 0x03, 0xb9, 0x03, 0xdd, 0x03, 0x99, 0x03, 0x71, 0x03, 0x55, + 0x02, 0x3d, 0x02, 0x85, 0x04, 0xb5, 0x04, 0x59, 0x04, 0xd9, 0x00, 0xe5, 0x04, + 0x69, 0x04, 0xa5, 0x04, 0xf5, 0x04, 0xd5, 0x04, 0x29, 0x01, 0x51, 0x01, 0x61, + 0x01, 0x75, 0x01, 0x49, 0x04, 0x39, 0x04, 0x9d, 0x01, 0x8d, 0x01, 0xfd, 0x01, + 0xf5, 0x00, 0x0d, 0x01, 0xbd, 0x00, 0x29, 0x04, 0x19, 0x04, 0xad, 0x01, 0xd5, + 0x01, 0x0d, 0x02, 0xc1, 0x01, 0xe9, 0x01, 0xe1, 0x00, 0xfd, 0x03, 0x3d, 0x01, + 0x1d, 0x01, 0x29, 0x02, 0x06, 0x40, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x00, 0x02, + 0x60, 0x00, 0x00, 0x04, 0x40, 0x43, 0x53, 0x50, 0x00, 0x00, 0x00, 0x04, 0x40, + 0x43, 0x47, 0x50, 0x00, 0x00, 0x00, 0x04, 0x40, 0x50, 0x43, 0x43, 0x00, 0x00, + 0x00, 0x0e, 0x40, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, + 0x6f, 0x76, 0x65, 0x00, 0x02, 0x60, 0x01, 0x00, 0x10, 0x40, 0x6c, 0x6f, 0x61, + 0x64, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x00, + 0x00, 0x00, 0x02, 0x60, 0x02, 0x00, 0x09, 0x40, 0x6c, 0x6f, 0x61, 0x64, 0x5f, + 0x69, 0x6e, 0x74, 0x00, 0x00, 0x02, 0x60, 0x03, 0x00, 0x06, 0x40, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x00, 0x02, 0x60, 0x04, 0x00, 0x0c, 0x40, 0x67, 0x65, 0x74, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x00, 0x00, 0x00, 0x02, 0x60, + 0x05, 0x00, 0x0c, 0x40, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x00, 0x00, 0x00, 0x02, 0x60, 0x06, 0x00, 0x09, 0x40, 0x67, 0x65, + 0x74, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x00, 0x00, 0x02, 0x60, 0x07, 0x00, 0x0b, + 0x40, 0x67, 0x65, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x60, 0x08, 0x00, 0x10, 0x40, 0x67, 0x65, 0x74, 0x5f, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, + 0x02, 0x60, 0x09, 0x00, 0x07, 0x40, 0x6c, 0x65, 0x64, 0x5f, 0x6f, 0x6e, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x60, 0x0a, 0x00, 0x08, 0x40, 0x6c, 0x65, 0x64, 0x5f, + 0x6f, 0x66, 0x66, 0x00, 0x00, 0x00, 0x02, 0x60, 0x0b, 0x00, 0x0c, 0x40, 0x72, + 0x65, 0x61, 0x64, 0x5f, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x00, 0x00, 0x00, + 0x02, 0x60, 0x0c, 0x00, 0x0c, 0x40, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73, 0x77, + 0x69, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, 0x02, 0x60, 0x0d, 0x00, 0x0d, 0x40, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x73, 0x00, + 0x00, 0x02, 0x60, 0x0e, 0x00, 0x0e, 0x40, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73, + 0x77, 0x69, 0x74, 0x63, 0x68, 0x65, 0x73, 0x00, 0x02, 0x60, 0x0f, 0x00, 0x08, + 0x40, 0x6c, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x74, 0x00, 0x00, 0x00, 0x02, 0x60, + 0x10, 0x00, 0x13, 0x40, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, + 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x60, 0x11, 0x00, 0x0b, 0x40, 0x75, 0x61, 0x72, 0x74, 0x5f, 0x77, 0x72, + 0x69, 0x74, 0x65, 0x00, 0x00, 0x00, 0x00, 0x02, 0x60, 0x12, 0x00, 0x16, 0x40, + 0x44, 0x41, 0x54, 0x41, 0x5f, 0x53, 0x45, 0x4e, 0x53, 0x4f, 0x52, 0x5f, 0x54, + 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x00, 0x13, 0x40, 0x44, 0x41, + 0x54, 0x41, 0x5f, 0x53, 0x45, 0x4e, 0x53, 0x4f, 0x52, 0x5f, 0x53, 0x41, 0x4d, + 0x50, 0x4c, 0x45, 0x00, 0x00, 0x00, 0x00, 0x17, 0x40, 0x44, 0x41, 0x54, 0x41, + 0x5f, 0x47, 0x52, 0x49, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x41, 0x47, 0x45, 0x5f, + 0x53, 0x54, 0x41, 0x52, 0x54, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x40, 0x44, 0x41, + 0x54, 0x41, 0x5f, 0x47, 0x52, 0x49, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x41, 0x47, + 0x45, 0x5f, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x00, 0x1d, 0x40, + 0x44, 0x41, 0x54, 0x41, 0x5f, 0x47, 0x52, 0x49, 0x44, 0x5f, 0x52, 0x45, 0x51, + 0x55, 0x45, 0x53, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, + 0x4d, 0x45, 0x00, 0x00, 0x1b, 0x40, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x47, 0x52, + 0x49, 0x44, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x5f, 0x44, 0x55, + 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x40, 0x44, + 0x41, 0x54, 0x41, 0x5f, 0x47, 0x52, 0x49, 0x44, 0x5f, 0x52, 0x45, 0x51, 0x55, + 0x45, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x00, + 0x00, 0x00, 0x00, 0x25, 0x40, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x50, 0x52, 0x4f, + 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, + 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x5f, 0x44, + 0x41, 0x59, 0x00, 0x00, 0x1c, 0x40, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x50, 0x52, + 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, + 0x4c, 0x45, 0x5f, 0x52, 0x41, 0x54, 0x45, 0x00, 0x00, 0x00, 0x22, 0x40, 0x44, + 0x41, 0x54, 0x41, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, + 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x5f, 0x52, 0x41, 0x54, 0x45, + 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x00, 0x26, 0x40, 0x44, 0x41, 0x54, 0x41, + 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x56, 0x41, 0x52, + 0x49, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, + 0x4d, 0x50, 0x5f, 0x42, 0x41, 0x53, 0x45, 0x00, 0x1d, 0x40, 0x44, 0x41, 0x54, + 0x41, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x56, 0x41, + 0x52, 0x49, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x00, + 0x00, 0x20, 0x40, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, + 0x44, 0x45, 0x52, 0x5f, 0x56, 0x41, 0x52, 0x49, 0x41, 0x4e, 0x43, 0x45, 0x5f, + 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x00, 0x00, 0x00, 0x1c, 0x40, + 0x44, 0x41, 0x54, 0x41, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, + 0x5f, 0x56, 0x41, 0x52, 0x49, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x52, 0x41, 0x54, + 0x45, 0x00, 0x00, 0x00, 0x18, 0x40, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x0e, 0x40, 0x72, 0x61, 0x74, 0x65, 0x5f, + 0x76, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x00, 0x0e, 0x40, 0x72, 0x61, + 0x74, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x00, 0x0d, + 0x40, 0x67, 0x72, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x00, 0x00, 0x0c, 0x40, 0x67, 0x72, 0x69, 0x64, 0x5f, 0x6f, 0x75, 0x74, 0x61, + 0x67, 0x65, 0x00, 0x00, 0x00, 0x0d, 0x40, 0x48, 0x4f, 0x55, 0x52, 0x5f, 0x53, + 0x45, 0x43, 0x4f, 0x4e, 0x44, 0x53, 0x00, 0x00, 0x05, 0x40, 0x53, 0x54, 0x53, + 0x20, 0x00, 0x00, 0x07, 0x40, 0x20, 0x52, 0x41, 0x54, 0x45, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x40, 0x20, 0x52, 0x45, 0x51, 0x20, 0x00, 0x1e, 0x40, 0x47, + 0x72, 0x69, 0x64, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x6c, + 0x65, 0x73, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x00, 0x0c, 0x40, 0x62, 0x61, 0x74, 0x74, 0x65, 0x72, 0x79, 0x20, + 0x2d, 0x31, 0x0a, 0x00, 0x00, 0x00, 0x1e, 0x40, 0x47, 0x72, 0x69, 0x64, 0x20, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x6d, 0x6f, 0x72, 0x65, 0x20, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x0b, + 0x40, 0x62, 0x61, 0x74, 0x74, 0x65, 0x72, 0x79, 0x20, 0x31, 0x0a, 0x00, 0x00, + 0x00, 0x00, 0x0e, 0x40, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, + 0x66, 0x72, 0x65, 0x65, 0x00, 0x0b, 0x40, 0x62, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x20, 0x30, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x05, 0x50, 0x01, 0x31, 0x32, + 0xe5, 0xa0, 0x10, 0x06, 0xe0, 0x10, 0x70, 0x10, 0x67, 0x10, 0x08, 0x88, 0x63, + 0x00, 0xe6, 0x89, 0x02, 0x00, 0x88, 0x59, 0x04, 0x6b, 0xe6, 0xe3, 0x70, 0x0a, + 0x10, 0x89, 0x02, 0x00, 0x88, 0x59, 0x04, 0x6b, 0xe8, 0x60, 0x02, 0x60, 0x00, + 0x07, 0x50, 0x01, 0x01, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, + 0xa1, 0x89, 0x01, 0x00, 0x88, 0xb9, 0x03, 0x6b, 0x33, 0x78, 0x03, 0xa1, 0x11, + 0x06, 0x6d, 0x70, 0x3a, 0x32, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, + 0x6b, 0xa1, 0x89, 0x01, 0x00, 0x88, 0x99, 0x03, 0x6b, 0x33, 0x78, 0x03, 0x6c, + 0xa0, 0x31, 0x11, 0xe0, 0x10, 0x70, 0x06, 0x67, 0x31, 0x11, 0x13, 0x6c, 0xe1, + 0x70, 0x17, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, 0x89, + 0x01, 0x00, 0x88, 0xdd, 0x03, 0x6b, 0x33, 0x78, 0x03, 0x60, 0x02, 0x60, 0x02, + 0x60, 0x09, 0x50, 0x01, 0x88, 0x19, 0x00, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, + 0x0d, 0x02, 0x6b, 0xa1, 0x89, 0x01, 0x00, 0x88, 0x71, 0x03, 0x6b, 0x06, 0x78, + 0x03, 0xa1, 0x11, 0x06, 0x6e, 0x70, 0x27, 0x89, 0x02, 0x00, 0x88, 0xfd, 0x03, + 0x6b, 0x01, 0x31, 0x32, 0x78, 0x03, 0xa0, 0x10, 0x02, 0x6d, 0x70, 0x49, 0x01, + 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, 0x89, 0x01, 0x00, 0x88, + 0x2d, 0x03, 0x6b, 0x13, 0x78, 0x03, 0x60, 0x88, 0x19, 0x00, 0x88, 0x19, 0x00, + 0x89, 0x02, 0x00, 0x88, 0x19, 0x04, 0x6b, 0x01, 0x31, 0x16, 0x06, 0x78, 0x04, + 0xa1, 0x11, 0x02, 0x6e, 0x70, 0x17, 0x89, 0x02, 0x00, 0x88, 0x19, 0x04, 0x6b, + 0x01, 0x31, 0x16, 0x07, 0x78, 0x04, 0xa0, 0x10, 0x02, 0x6e, 0x70, 0x06, 0x80, + 0x02, 0x76, 0xa9, 0x11, 0x60, 0x10, 0x60, 0x02, 0x60, 0x00, 0x00, 0x00, 0x06, + 0x50, 0x01, 0x01, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, + 0x89, 0x01, 0x00, 0x88, 0xc5, 0x02, 0x6b, 0x06, 0x78, 0x03, 0xa1, 0x11, 0x06, + 0x6d, 0x70, 0x38, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, + 0x89, 0x01, 0x00, 0x88, 0xa5, 0x02, 0x6b, 0x06, 0x78, 0x03, 0xa0, 0x31, 0x11, + 0xe0, 0x10, 0x70, 0x06, 0x67, 0x31, 0x11, 0x13, 0x6c, 0xe1, 0x70, 0x17, 0x01, + 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, 0x89, 0x01, 0x00, 0x88, + 0xe5, 0x02, 0x6b, 0x06, 0x78, 0x03, 0x60, 0x02, 0x60, 0x02, 0x60, 0x00, 0x00, + 0x08, 0x50, 0x01, 0x01, 0x01, 0x89, 0x01, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, + 0xa1, 0x89, 0x01, 0x00, 0x88, 0x89, 0x02, 0x6b, 0x06, 0x78, 0x03, 0xa1, 0x11, + 0x06, 0x6e, 0x70, 0x02, 0x01, 0x60, 0x88, 0x19, 0x00, 0x01, 0x89, 0x01, 0x00, + 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, 0x89, 0x01, 0x00, 0x88, 0x6d, 0x02, 0x6b, + 0x06, 0x78, 0x03, 0xa1, 0x11, 0x11, 0x6c, 0xa0, 0x10, 0x32, 0xe0, 0x70, 0x1b, + 0x11, 0x32, 0xe0, 0x70, 0x18, 0x11, 0x31, 0x08, 0x88, 0x63, 0x00, 0xe6, 0x89, + 0x02, 0x00, 0x88, 0x59, 0x04, 0x6b, 0xe6, 0x6c, 0xe1, 0x70, 0x07, 0x67, 0x76, + 0xc2, 0x02, 0x60, 0x02, 0x60, 0x02, 0x60, 0x00, 0x00, 0x0c, 0x50, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x89, 0x00, 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, 0x89, + 0x00, 0x00, 0x88, 0x3d, 0x02, 0x6b, 0x06, 0x78, 0x03, 0xa3, 0x01, 0x89, 0x00, + 0x00, 0x10, 0x88, 0x0d, 0x02, 0x6b, 0xa1, 0x89, 0x00, 0x00, 0x88, 0x05, 0x03, + 0x6b, 0x06, 0x78, 0x03, 0xa2, 0x01, 0x89, 0x03, 0x00, 0x10, 0x88, 0x29, 0x04, + 0x6b, 0xa1, 0x15, 0x15, 0x78, 0x03, 0xa1, 0x01, 0x89, 0x03, 0x00, 0x10, 0x88, + 0x39, 0x04, 0x6b, 0xa1, 0x15, 0x78, 0x02, 0xa0, 0x01, 0x89, 0x00, 0x00, 0x10, + 0x88, 0xbd, 0x00, 0x6b, 0xa1, 0x88, 0x69, 0x04, 0x16, 0x88, 0x71, 0x04, 0x16, + 0x88, 0x7d, 0x04, 0x17, 0x78, 0x87, 0x10, 0x02, 0x6e, 0x70, 0x16, 0x11, 0x06, + 0xe0, 0x70, 0x56, 0x01, 0x89, 0x00, 0x00, 0x10, 0x88, 0x29, 0x02, 0x6b, 0xa1, + 0x88, 0xf5, 0x04, 0x78, 0x82, 0x01, 0x60, 0x10, 0x06, 0xe0, 0x70, 0x20, 0x01, + 0x89, 0x00, 0x00, 0x10, 0x88, 0xbd, 0x00, 0x6b, 0xa1, 0x88, 0xb5, 0x04, 0x78, + 0x82, 0x01, 0x89, 0x00, 0x00, 0x10, 0x88, 0x29, 0x02, 0x6b, 0xa1, 0x88, 0xd5, + 0x04, 0x78, 0x82, 0x01, 0x60, 0x01, 0x89, 0x00, 0x00, 0x10, 0x88, 0xbd, 0x00, + 0x6b, 0xa1, 0x88, 0x85, 0x04, 0x78, 0x82, 0x01, 0x89, 0x00, 0x00, 0x10, 0x88, + 0x29, 0x02, 0x6b, 0xa1, 0x88, 0xa5, 0x04, 0x78, 0x82, 0x76, 0xde, 0x01, 0x89, + 0x00, 0x00, 0x10, 0x88, 0xbd, 0x00, 0x6b, 0xa1, 0x88, 0xe5, 0x04, 0x78, 0x82, + 0x01, 0x89, 0x00, 0x00, 0x10, 0x88, 0x29, 0x02, 0x6b, 0xa1, 0x88, 0xd5, 0x04, + 0x78, 0x82, 0x01, 0x60, 0x00, 0x02, 0x00, 0x02, 0x00, 0x98, 0x00, 0x98, 0x00, + 0x01, 0x00, 0x94, 0xc0, 0x05, 0x00, 0x05, 0x00, 0xbd, 0x00, 0xc5, 0x00, 0xc9, + 0x00, 0x23, 0x00, 0xd1, 0x00, 0x27, 0x00, 0xd9, 0x00, 0x2b, 0x00, 0xe1, 0x00, + 0xf1, 0x00, 0xf5, 0x00, 0x09, 0x01, 0x0d, 0x01, 0x19, 0x01, 0x1d, 0x01, 0x25, + 0x01, 0x29, 0x01, 0x39, 0x01, 0x3d, 0x01, 0x4d, 0x01, 0x51, 0x01, 0x5d, 0x01, + 0x61, 0x01, 0x71, 0x01, 0x75, 0x01, 0x89, 0x01, 0x8d, 0x01, 0x99, 0x01, 0x9d, + 0x01, 0xa9, 0x01, 0xad, 0x01, 0xbd, 0x01, 0xc1, 0x01, 0xd1, 0x01, 0xd5, 0x01, + 0xe5, 0x01, 0xe9, 0x01, 0xf9, 0x01, 0xfd, 0x01, 0x09, 0x02, 0x0d, 0x02, 0x25, + 0x02, 0x29, 0x02, 0x39, 0x02, 0x3d, 0x02, 0x07, 0x00, 0x55, 0x02, 0x0b, 0x00, + 0x6d, 0x02, 0x0f, 0x00, 0x89, 0x02, 0x13, 0x00, 0xa5, 0x02, 0x17, 0x00, 0xc5, + 0x02, 0x1b, 0x00, 0xe5, 0x02, 0x1f, 0x00, 0x05, 0x03, 0x23, 0x00, 0x2d, 0x03, + 0x27, 0x00, 0x4d, 0x03, 0x2b, 0x00, 0x71, 0x03, 0x2f, 0x00, 0x99, 0x03, 0x33, + 0x00, 0xb9, 0x03, 0x37, 0x00, 0xdd, 0x03, 0x3b, 0x00, 0x1c, 0xc0, 0x05, 0x00, + 0x05, 0x00, 0xfd, 0x03, 0x05, 0x05, 0x19, 0x04, 0x31, 0x05, 0x29, 0x04, 0x8d, + 0x05, 0x39, 0x04, 0x0d, 0x06, 0x49, 0x04, 0x69, 0x06, 0x59, 0x04, 0x43, 0x38}; +unsigned int hello_mvm_bc_len = sizeof(hello_mvm_bc); diff --git a/smartmeter/cheriot/grid.cc b/smartmeter/cheriot/grid.cc new file mode 100644 index 0000000..eaf3ddc --- /dev/null +++ b/smartmeter/cheriot/grid.cc @@ -0,0 +1,367 @@ +// Copyright SCI Semiconductor and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +/* + * The grid compartment. + * + * Maintains a MQTT connection and reports sensor data + */ + +#define CHERIOT_NO_AMBIENT_MALLOC + +#include "common.hh" + +#include +#include +#include +#include +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY +# include +#endif +#include +#include +#include +#include +#include + +#include MQTT_BROKER_ANCHOR + +DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(GridMallocCapability, 24 * 1024); +#define MALLOC_CAPABILITY STATIC_SEALED_VALUE(GridMallocCapability) + +using CHERI::Capability; + +using Debug = ConditionalDebug; + +/// Maximum permitted MQTT client identifier length (from the MQTT +/// specification) +constexpr size_t MQTTMaximumClientLength = 23; +/// Prefix for MQTT client identifier +constexpr std::string_view clientIDPrefix{"cheriotsmq"}; +/// Space for the random client ID. +static std::array + clientID; +static_assert(clientID.size() <= MQTTMaximumClientLength); + +// MQTT network buffer sizes +constexpr const size_t networkBufferSize = 1024; +constexpr const size_t incomingPublishCount = 2; +constexpr const size_t outgoingPublishCount = 2; + +DECLARE_AND_DEFINE_CONNECTION_CAPABILITY(MQTTConnectionRightsGrid, + MQTT_BROKER_HOST, + 8883, + ConnectionTypeTCP); + +constexpr std::string_view outageTopicPrefix{"cheriot-smartmeter/g/outage/"}; +static std::array + outageTopic; +constexpr std::string_view requestTopicPrefix{"cheriot-smartmeter/g/request/"}; +static std::array + requestTopic; +constexpr std::string_view publishTopicPrefix{"cheriot-smartmeter/g/update/"}; +static std::array + publishTopic; +constexpr std::string_view crashTopicPrefix{"cheriot-smartmeter/g/crash/"}; +static std::array + crashTopic; + +static void __cheri_callback publishCallback(const char *topicName, + size_t topicNameLength, + const void *payload, + size_t payloadLength) +{ + // Check input pointers (can be skipped if the MQTT library is trusted) + Timeout t{MS_TO_TICKS(5000)}; + if (heap_claim_ephemeral(&t, topicName) != 0 || + !CHERI::check_pointer(topicName, topicNameLength)) + { + Debug::log( + "Cannot claim or verify PUBLISH callback topic name pointer."); + return; + } + + if (heap_claim_ephemeral(&t, payload) != 0 || + !CHERI::check_pointer(payload, payloadLength)) + { + Debug::log("Cannot claim or verify PUBLISH callback payload pointer."); + return; + } + + auto topicView = std::string_view{topicName, topicNameLength}; + auto payloadView = + std::string_view{static_cast(payload), payloadLength}; + + if (topicView == std::string_view{outageTopic.data(), outageTopic.size()}) + { + char buf[22]; // uint32_t is up to 10 decimal chars; add SP and NUL + + Debug::log("Got outage PUBLISH: {}", payloadView); + + if (payloadLength >= sizeof(buf)) + { + Debug::log("Overlong outage PUBLISH, discarding"); + return; + } + memcpy(buf, payload, payloadLength); + buf[payloadLength] = '\0'; + + struct grid_planned_outage_payload payload; + char *ptr; + payload.start_time = strtoul(buf, &ptr, 10); + payload.duration = strtoul(ptr, nullptr, 10); + + auto gridOutage = SHARED_OBJECT_WITH_PERMISSIONS( + grid_planned_outage, grid_planned_outage, true, true, false, false); + gridOutage->write(payload); + } + if (topicView == std::string_view{requestTopic.data(), requestTopic.size()}) + { + /* + * 10 digits for 32-bit timestamp_day, space, unsigned 16-bit number (5 + * digits), space, signed 16-bit number (5 digits + 1 sign), and a NUL + * byte. + */ + char buf[10 + 1 + 5 + 1 + 6 + 1]; + + Debug::log("Got request PUBLISH: {}", payloadView); + + if (payloadLength >= sizeof(buf)) + { + Debug::log("Overlong request PUBLISH, discarding"); + return; + } + memcpy(buf, payload, payloadLength); + buf[payloadLength] = '\0'; + + struct grid_request_payload payload; + char *ptr; + payload.start_time = strtoul(buf, &ptr, 10); + payload.duration = strtoul(ptr, &ptr, 10); + payload.severity = strtol(ptr, nullptr, 10); + + auto gridRequest = SHARED_OBJECT_WITH_PERMISSIONS( + grid_request, grid_request, true, true, false, false); + gridRequest->write(payload); + } + else + { + Debug::log("Unknown topic in PUBLISH callback: {}", topicView); + } +} + +int grid_entry() +{ + int ret; + Timeout noTimeout{UnlimitedTimeout}; + + Debug::log("entry"); + housekeeping_initial_barrier(); + Debug::log("initialization barrier down"); + + const char *mqttName = housekeeping_mqtt_unique_get(); + HOUSEKEEPING_MQTT_CONCAT(clientID, clientIDPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(crashTopic, crashTopicPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(outageTopic, outageTopicPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(publishTopic, publishTopicPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(requestTopic, requestTopicPrefix, mqttName); + +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY + auto sensorDataFine = SHARED_OBJECT_WITH_PERMISSIONS( + sensor_data_fine, sensor_data_fine, true, false, false, false); +#else + auto sensorDataFine = &theData.sensor_data_fine; +#endif + + auto userCrashCount = SHARED_OBJECT_WITH_PERMISSIONS( + user_crash_count, user_crash_count, true, false, false, false); + + while (true) + { + int tick = 0; + + Debug::log("Connecting to MQTT broker..."); + + Timeout connectTimeout{MS_TO_TICKS(30000)}; + + MQTTConnection handle = + mqtt_connect(&connectTimeout, + MALLOC_CAPABILITY, + CONNECTION_CAPABILITY(MQTTConnectionRightsGrid), + publishCallback, + nullptr /* XXX should watch our ACK stream */, + TAs, + TAs_NUM, + networkBufferSize, + incomingPublishCount, + outgoingPublishCount, + clientID.data(), + clientID.size()); + + if (!Capability{handle}.is_valid()) + { + Debug::log("Failed to connect."); + goto retry; + } + + Debug::log("Connected to MQTT broker!"); + + ret = mqtt_subscribe(&connectTimeout, + handle, + 1, // QoS 1 = delivered at least once + outageTopic.data(), + outageTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe for outages: {}", ret); + goto retry; + } + + // XXX clobbers packet ID; we should be watching our ACK stream + ret = mqtt_subscribe(&connectTimeout, + handle, + 1, // QoS 1 = delivered at least once + requestTopic.data(), + requestTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe for requests: {}", ret); + goto retry; + } + + { + sensor_data_fine localSensorData = {0}; + user_crash_count localCrashData = {0}; + + Timeout loopTimeout{MS_TO_TICKS(5000)}; + while (true) + { + ret = mqtt_run(&loopTimeout, handle); + + if (ret < 0) + { + Debug::log("Failed to run MQTT, error {}; hanging up", ret); + goto retry; + } + else if (loopTimeout.remaining == 0) + { + /* + * XXX We'd love to be multi-waiting on the network / MQTT + * and the sensorData->timestamp, but the APIs we have make + * that harder than it should be. + */ + + // sensor data + { + Timeout readTimeout{MS_TO_TICKS(1000)}; + ret = sensorDataFine->read(&readTimeout, + localSensorData.version, + localSensorData.payload); + if (ret == 0) + { + Debug::log("Awake and publishing {}", + localSensorData.version); + + /* + * 10 digits for 3 32-bit values, space separated, + * with NUL terminator: the timestamp and two most + * recent values. + */ + char msg[(10 + 1) * 3]; + ssize_t msglen = + snprintf(msg, + sizeof(msg), + "%d %d %d", + localSensorData.payload.timestamp, + localSensorData.payload.samples[0], + localSensorData.payload.samples[1]); + + Timeout t{MS_TO_TICKS(5000)}; + ret = + mqtt_publish(&t, + handle, + 1, // QoS 1 = delivered at least once + publishTopic.data(), + publishTopic.size(), + msg, + msglen); + + if (ret < 0) + { + Debug::log("Failed to publish, error {}.", ret); + goto retry; + } + } + else + { + Debug::log("Awake but skipping publish: {}", ret); + } + } + + // crash count + { + Timeout readTimeout{MS_TO_TICKS(1000)}; + ret = userCrashCount->read(&readTimeout, + localCrashData.version, + localCrashData.payload); + if (ret == 0) + { + Debug::log( + "Awake and publishing crash notification {}", + localCrashData.version); + + /* + * 10 digits for a 32-bit value and NUL terminator + */ + char msg[10 + 1]; + ssize_t msglen = snprintf( + msg, + sizeof(msg), + "%d", + localCrashData.payload.crashes_since_boot); + + Timeout t{MS_TO_TICKS(5000)}; + ret = + mqtt_publish(&t, + handle, + 1, // QoS 1 = delivered at least once + crashTopic.data(), + crashTopic.size(), + msg, + msglen); + + if (ret < 0) + { + Debug::log("Failed to publish, error {}.", ret); + goto retry; + } + } + else if (ret != -ENOMSG) + { + Debug::log("Unexpected crash count read result: {}", + ret); + } + } + + loopTimeout = Timeout{MS_TO_TICKS(5000)}; + } + } + } + + retry: + if (Capability{handle}.is_valid()) + { + mqtt_disconnect(&noTimeout, MALLOC_CAPABILITY, handle); + } + + Timeout t{MS_TO_TICKS(5000)}; + thread_sleep(&t, ThreadSleepNoEarlyWake); + } + + return 0; +} diff --git a/smartmeter/cheriot/housekeeping.cc b/smartmeter/cheriot/housekeeping.cc new file mode 100644 index 0000000..07e8efc --- /dev/null +++ b/smartmeter/cheriot/housekeeping.cc @@ -0,0 +1,190 @@ +// Copyright SCI Semiconductor and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +/* + * Housekeeping compartment. + * + * Performs device initialization and periodic SNTP resync. + */ + +#include "common.hh" + +#include +#include +#include +#include +#include +#include +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY +# include +#endif +#include +#include +#include +#include +#include +#include + +using Debug = ConditionalDebug; + +std::atomic initialized; + +char mqttUnique[8]; + +constexpr bool random_id = std::string_view(MQTT_UNIQUE_ID) == "random"; +// MQTT_UNIQUE_ID must either be "random" or axactly 8 A-Za-z0-9 characters +// (no null byte) +constexpr bool is_valid_id(std::string_view id) +{ + if (id == "random") + return true; + + if (id.size() != sizeof(mqttUnique)) + { + return false; + } + + for (char c : id) + { + switch (c) + { + default: + return false; + case '0' ... '9': + case 'a' ... 'z': + case 'A' ... 'Z': + continue; + } + } + return true; +} +static_assert(is_valid_id(MQTT_UNIQUE_ID)); + +static void do_sntp() +{ + /* + * The first time here is almost surely the first time the network stack + * has been asked to send something. Unless things change (see + * https://github.com/FreeRTOS/FreeRTOS-Plus-TCP/issues/1244), it is going + * to swallow our UDP datagrams while it figures out ARP. Don't wait very + * long the first time, or between attempts, but give subsequent attempts + * longer to complete. + * + * But don't set the timeout too small, so that our periodic refreshes have + * a chance of doing the right thing without retries. + */ + Timeout t{MS_TO_TICKS(1000)}; + + // SNTP must be run for the TLS stack to be able to check certificate dates. + while (sntp_update(&t) != 0) + { + Debug::log("Failed to update NTP time"); + + t = Timeout{MS_TO_TICKS(1000)}; + thread_sleep(&t, ThreadSleepNoEarlyWake); + + t = Timeout{MS_TO_TICKS(5000)}; + } + Debug::log("Updating NTP took {} ticks", t.elapsed); + + { + timeval tv; + int ret = gettimeofday(&tv, nullptr); + if (ret != 0) + { + Debug::log("Failed to get time of day: {}", ret); + } + else + { + // Truncate the epoch time to 32 bits for printing. + Debug::log("Current UNIX epoch time: {}", (int32_t)tv.tv_sec); + } + } +} + +void housekeeping_initial_barrier() +{ + while (initialized.load() == 0) + { + initialized.wait(0); + } +} + +const char *housekeeping_mqtt_unique_get() +{ + CHERI::Capability ret{mqttUnique}; + ret.bounds() = sizeof(mqttUnique); + ret.permissions() &= CHERI::Permission::Load; + return ret.get(); +} + +int housekeeping_entry() +{ + Debug::log("entry"); + + Debug::log("Configuring pinmux"); + auto pinSinks = MMIO_CAPABILITY(SonataPinmux::PinSinks, pinmux_pins_sinks); + pinSinks->get(SonataPinmux::PinSink::pmod0_2) + .select(4); // uart1 tx -> pmod0_2 + auto blockSinks = + MMIO_CAPABILITY(SonataPinmux::BlockSinks, pinmux_block_sinks); + blockSinks->get(SonataPinmux::BlockSink::uart_1_rx) + .select(5); // pmod0_3 -> uart1 rx + + Debug::log("Initialising UART1 with baud 9600"); + auto uart1 = MMIO_CAPABILITY(Uart, uart1); + uart1->init(9600); + + uart1->receive_watermark(OpenTitanUart::ReceiveWatermark::Level1); + uart1->interrupt_enable(OpenTitanUart::InterruptReceiveWatermark); + + if constexpr (random_id) + { + mqtt_generate_client_id(mqttUnique, sizeof(mqttUnique)); + } + else + { + // copy the configured MQTT_UNIQUE_ID excluding the null byte + memcpy(mqttUnique, MQTT_UNIQUE_ID, sizeof(mqttUnique)); + } + + { + std::string_view mqttUniqueView{mqttUnique, sizeof(mqttUnique)}; + Debug::log("MQTT Unique: {}", mqttUniqueView); + } + + network_start(); + + Debug::log("network started"); + + do_sntp(); + + initialized.store(1); + initialized.notify_all(); + + Debug::log("initialization barrier released"); + + while (1) + { + int tick = 0; + + Timeout t{MS_TO_TICKS(60000)}; + thread_sleep(&t, ThreadSleepNoEarlyWake); + tick++; + + /* Every so often (16 minutes, presently), resync our clock */ + if ((tick & 0xF) == 0) + { + do_sntp(); + } + + /* + * For more useful reporting, flush the quarantine. + * + * XXX Should we make heap_available report the sum? Separately expose + * the quarantine size? + */ + heap_quarantine_empty(); + Debug::log("Heap available: {}", heap_available()); + } +} diff --git a/smartmeter/cheriot/js/compile.sh b/smartmeter/cheriot/js/compile.sh new file mode 100755 index 0000000..7deabbe --- /dev/null +++ b/smartmeter/cheriot/js/compile.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -e + +unset HEADER_OUT + +while getopts "H:" o; do + case "$o" in + H) HEADER_OUT="${OPTARG}";; + *) exit 1; + esac +done +shift $((OPTIND - 1)) + +if [ -z "$1" ] ; then + echo Usage: $0 {javascript file} + exit 1 +fi + +if [ ! -f ./node_modules/.bin/microvium ] ; then + if ! type npm >/dev/null 2>&1 ; then + if type apt >/dev/null 2>&1 ; then + echo npm is not installed. Trying to install it from apt. + echo If you are not using the devcontainer, this may fail. + sudo apt update + sudo apt install nodejs make gcc g++ + else + echo npm is not installed. Please install it. + exit 1 + fi + fi + npm install microvium +fi + +declare -a MVM_CMD +MVM_CMD=(./node_modules/.bin/microvium "$1") + +[ -n "${HEADER_OUT-}" ] && { + MVM_CMD+=("--output-bytes") + MVM_CMD+=(">>") + MVM_CMD+=("${HEADER_OUT}") + + cat > "${HEADER_OUT}" <> "${HEADER_OUT}" <${c}`; + + userjs_snapshot_fetch += `\tcase ${name}:\n`; + if (arrayish === "index") + { + userjs_snapshot_fetch += `\t\tif ((index < 0) || (index >= (sizeof(${fullc})/sizeof(${fullc}[0]))))\n`; + userjs_snapshot_fetch += `\t\t{\n`; + userjs_snapshot_fetch += `\t\t\treturn 0;\n`; + userjs_snapshot_fetch += `\t\t}\n`; + userjs_snapshot_fetch += `\t\treturn ${fullc}[index];\n`; + } + else if (arrayish === "expose") + { + userjs_snapshot_fetch += `\t\tregister_write(index, &${fullc});\n`; + userjs_snapshot_fetch += `\t\treturn 0;\n`; + } + else + { + userjs_snapshot_fetch += `\t\treturn ${fullc};\n`; + } +} + +add_userjs_snapshot_index("DATA_SENSOR_TIMESTAMP", "sensor_data.timestamp", "no"); +add_userjs_snapshot_index("DATA_SENSOR_SAMPLE", "sensor_data.samples", "index"); +add_userjs_snapshot_index("DATA_GRID_OUTAGE_START", "grid_outage.start_time", "no"); +add_userjs_snapshot_index("DATA_GRID_OUTAGE_DURATION", "grid_outage.duration", "no"); +add_userjs_snapshot_index("DATA_GRID_REQUEST_START_TIME", "grid_request.start_time", "no"); +add_userjs_snapshot_index("DATA_GRID_REQUEST_DURATION", "grid_request.duration", "no"); +add_userjs_snapshot_index("DATA_GRID_REQUEST_SEVERITY", "grid_request.severity", "no"); +add_userjs_snapshot_index("DATA_PROVIDER_SCHEDULE_TIMESTAMP_DAY", "provider_schedule.timestamp_day", "no"); +add_userjs_snapshot_index("DATA_PROVIDER_SCHEDULE_RATE", "provider_schedule.rate", "index"); +add_userjs_snapshot_index("DATA_PROVIDER_SCHEDULE_RATE_ARRAY", "provider_schedule.rate", "expose"); +add_userjs_snapshot_index("DATA_PROVIDER_VARIANCE_TIMESTAMP_BASE", "provider_variance.timestamp_base", "no"); +add_userjs_snapshot_index("DATA_PROVIDER_VARIANCE_START", "provider_variance.start", "index"); +add_userjs_snapshot_index("DATA_PROVIDER_VARIANCE_DURATION", "provider_variance.duration", "index"); +add_userjs_snapshot_index("DATA_PROVIDER_VARIANCE_RATE", "provider_variance.rate", "index"); + +userjs_snapshot_enum += '};\n' +userjs_snapshot_fetch += '\t}\n\treturn 0;\n}\n' + +fs.writeFileSync('../userffi.h', + header + + userjs_snapshot_enum + + userjs_snapshot_fetch); diff --git a/smartmeter/cheriot/js/sample_policy.js b/smartmeter/cheriot/js/sample_policy.js new file mode 100644 index 0000000..52285f5 --- /dev/null +++ b/smartmeter/cheriot/js/sample_policy.js @@ -0,0 +1,49 @@ +// Import everything from the environment +import * as host from "./ffi.js" +import * as sm from "./smartmeter.js" + +function run() +{ + var sts = host.read_from_snapshot(host.DATA_SENSOR_TIMESTAMP, 0); + + var schedule_day_change = host.read_from_snapshot(host.DATA_PROVIDER_SCHEDULE_TIMESTAMP_DAY, 0); + var rate = sm.rate_for_time(sts, schedule_day_change); + + var req = sm.grid_request(sts); + + host.print("STS ", sts, " RATE ", rate, " REQ ", req); + + if (req !== null) + { + if (req < 0) + { + // The grid is asking us to draw less; respond by discharging the battery + // Could also do things like turn off the heat pump + host.print("Grid request less consumption"); + host.uart_write("battery -1\n"); + } + else + { + // The grid is asking us to draw more; respond by charging the battery + // Could also do things like turn on the heat pump + host.print("Grid request more consumption"); + host.uart_write("battery 1\n"); + } + + // Grid requests take precedence over the rest of the policy + return; + } + + if (rate < 0) + { + // Electricity is very cheap and the grid isn't asking us to not draw + host.print("Power is free"); + host.uart_write("battery 1\n"); + return; + } + + // Default: neither charge nor discharge the battery. + host.uart_write("battery 0\n"); +} + +vmExport(1234, run); diff --git a/smartmeter/cheriot/js/smartmeter.js b/smartmeter/cheriot/js/smartmeter.js new file mode 100644 index 0000000..3679e0a --- /dev/null +++ b/smartmeter/cheriot/js/smartmeter.js @@ -0,0 +1,130 @@ +import * as host from "./ffi.js" + +export const HOUR_SECONDS = 3600; + +export function schedule_index_for_time(time, base) +{ + var delta = time - base; + + if ((delta < 0) || (delta >= 2*24*HOUR_SECONDS /* two days' seconds */)) + { + // Out of bounds + return null; + } + + return (delta / HOUR_SECONDS) | 0 /* truncated division */; +} + +export function rate_variance(time, base, index) +{ + var variance_duration = host.read_from_snapshot(host.DATA_PROVIDER_VARIANCE_DURATION, index); + + if (variance_duration === 0) + { + return null; + } + + var variance_start = base + host.read_from_snapshot(host.DATA_PROVIDER_VARIANCE_START, index); + + if ((time < variance_start) || (time > (variance_start + variance_duration))) + { + return null; + } + + return host.read_from_snapshot(host.DATA_PROVIDER_VARIANCE_RATE, index); +} + +export function rate_for_time(time, schedule_base) +{ + var variance_base = host.read_from_snapshot(host.DATA_PROVIDER_VARIANCE_TIMESTAMP_BASE, 0); + + if (variance_base !== 0) + { + // Do either of the variance slots cover this time? + let v0 = rate_variance(time, variance_base, 0); + if (v0 !== null) + { + // Yes, the 0th + return v0; + } + + let v1 = rate_variance(time, variance_base, 1); + if (v1 !== null) + { + // Yes, the 1st + return v1; + } + } + + // No; what's the scheduled rate? + let schedule_index = schedule_index_for_time(time, schedule_base) + if (schedule_index === null) + { + // Don't know; out of schedule bounds + return null; + } + + return host.read_from_snapshot(host.DATA_PROVIDER_SCHEDULE_RATE, schedule_index); +} + +export function grid_request(time) +{ + var req_duration = host.read_from_snapshot(host.DATA_GRID_REQUEST_DURATION, 0); + if (req_duration === 0) + { + return null; + } + + var req_start = host.read_from_snapshot(host.DATA_GRID_REQUEST_START_TIME, 0); + + if ((time < req_start) || (time > (req_start + req_duration))) + { + return null; + } + + return host.read_from_snapshot(host.DATA_GRID_REQUEST_SEVERITY, 0); +} + +// XXX: This is a WIP as opposed to anything useful. +// +// It should: return null if there's no scheduled outage within the 48 hour +// provider-specified window or an outage in progress, or a timestamp at which +// to begin charging the battery, between now and the start of the outage. If +// that's now, we should be charging the battery; if it's in the future, we +// should wait. +export function grid_outage(schedule_day_change, now) +{ + // Is there a grid planned outage in the near future? + var grid_outage_duration = host.read_from_snapshot(host.DATA_GRID_OUTAGE_DURATION, 0); + if (grid_outage_duration !== 0) + { + var grid_outage_start = host.read_from_snapshot(host.DATA_GRID_OUTAGE_START, 0); + let grid_outage_end = grid_outage_start + grid_outage_end; + + if (grid_outage_end < now) + { + // Past outage + return null; + } + else if (grid_outage_start < now) + { + // Ongoing outage; not much we can do about it now + return null; + } + else if (grid_outage_start > (schedule_day_change + 2*24*HOUR_SECONDS)) + { + // Far future outage, beyond the schedule's end + return null; + } + else + { + /* + * Outage within planning horizon. We want to ensure that we have + * power reserves when the time comes, so sweep through the schedule + * looking for a good time to charge. + * + * TODO + */ + } + } +} diff --git a/smartmeter/cheriot/microvium-ffi.hh b/smartmeter/cheriot/microvium-ffi.hh new file mode 100644 index 0000000..505dc48 --- /dev/null +++ b/smartmeter/cheriot/microvium-ffi.hh @@ -0,0 +1,595 @@ +#pragma once + +#include "common.hh" +#include +#include +#include +#include +#include +#include +#include + +#include + +/** + * Code related to the JavaScript interpreter. + */ +namespace +{ + using CHERI::Capability; + + /** + * Constants for functions exposed to JavaScript->C++ FFI + * + * The values here must match the ones used in cheri.js. + */ + enum Exports : mvm_HostFunctionID + { + Print = 1, + Move, + LoadCapability, + LoadInt, + Store, + GetAddress, + SetAddress, + GetBase, + GetLength, + GetPermissions, + LEDOn, + LEDOff, + ReadButton, + ReadSwitch, + ReadButtons, + ReadSwitches, + LEDSet, + ReadFromSnapshot, + UartWrite, + NetworkFaultInject + }; + + /// Constant for the run function exposed to C++->JavaScript FFI + static constexpr mvm_VMExportID ExportRun = 1234; + + /** + * Type used for the set of capabilities that JavaScript has complete + * control over. + */ + using AttackerRegisterState = std::array; + + /** + * Store the pointer to the on-stack registers state to CILS + */ + void state_set(AttackerRegisterState *rs) + { + invocation_state() = rs; + } + + /** + * Load the pointer to the on-stack register state from CILS + */ + AttackerRegisterState &state() + { + return *invocation_state(); + } + + /** + * Write a capability into one of the VM's register set. + */ + void register_write(int regno, void *value) + { + if ((regno >= 0) && (regno < 8)) + { + state()[regno] = value; + } + } + + /** + * Read a register from the VM's register set. This reads one of the 8 + * that can be written or CSP (8), CGP (9), or PCC (10). + */ + void *register_read(int regno) + { + switch (regno) + { + default: + return nullptr; + case 0 ... 7: + return state()[regno]; + case 8: + { + register void *cspRegister asm("csp"); + asm("" : "=C"(cspRegister)); + return cspRegister; + } + case 9: + { + register void *cgpRegister asm("cgp"); + asm("" : "=C"(cgpRegister)); + return cgpRegister; + } + case 10: + { + void *pcc; + asm("auipcc %0, 0\n" : "=C"(pcc)); + return pcc; + } + } + } + + /** + * Template that returns JavaScript argument specified in `arg` as a C++ + * type T. + */ + template + T extract_argument(mvm_VM *vm, mvm_Value arg); + + /** + * Specialisation to return integers. + */ + template<> + __always_inline int32_t extract_argument(mvm_VM *vm, mvm_Value arg) + { + return mvm_toInt32(vm, arg); + } + + /** + * Specialisation to return booleans. + */ + template<> + __always_inline bool extract_argument(mvm_VM *vm, mvm_Value arg) + { + return mvm_toBool(vm, arg); + } + + /** + * Populate a tuple with arguments from an array of JavaScript values. + * This uses `extract_argument` to coerce each JavaScript value to the + * expected type. + */ + template + __always_inline void + args_to_tuple(Tuple &tuple, mvm_VM *vm, mvm_Value *args) + { + if constexpr (Idx < std::tuple_size_v) + { + std::get(tuple) = extract_argument< + std::remove_reference_t(tuple))>>( + vm, args[Idx]); + args_to_tuple(tuple, vm, args); + } + } + + /** + * Helper template to extract the arguments from a function type. + */ + template + struct FunctionSignature; + + /** + * The concrete specialisation that decomposes the function type. + */ + template + struct FunctionSignature + { + /** + * A tuple type containing all of the argument types of the function + * whose type is being extracted. + */ + using ArgumentType = std::tuple; + }; + + /** + * The concrete specialisation that decomposes the function type. + */ + template + struct FunctionSignature + { + /** + * A tuple type containing all of the argument types of the function + * whose type is being extracted. + */ + using ArgumentType = std::tuple; + }; + + /** + * Call `Fn` with arguments from the Microvium arguments array. + * + * This is a wrapper that allows automatic forwarding from a function + * exported to JavaScript + */ + template + __always_inline mvm_TeError call_export(mvm_VM *vm, + mvm_Value *result, + mvm_Value *args, + uint8_t argsCount) + { + using TupleType = typename FunctionSignature< + std::remove_pointer_t>::ArgumentType; + // Return an error if we have the wrong number of arguments. + if (argsCount < std::tuple_size_v) + { + return MVM_E_UNEXPECTED; + } + // Get the arguments in a tuple. + TupleType arguments; + args_to_tuple(arguments, vm, args); + // If this returns void, we don't need to do anything with the return. + if constexpr (std::is_same_v) + { + std::apply(Fn, arguments); + } + else + { + // Coerce the return type to a JavaScript object of the correct + // type and return it. + auto primitiveResult = std::apply(Fn, arguments); + if constexpr (std::is_same_v) + { + *result = mvm_newBoolean(primitiveResult); + } + if constexpr (std::is_same_v) + { + *result = mvm_newInt32(vm, primitiveResult); + } + if constexpr (std::is_same_v) + { + *result = mvm_newString( + vm, primitiveResult.data(), primitiveResult.size()); + } + } + return MVM_E_SUCCESS; + } + + /** + * Helper that maps from Exports + */ + template + constexpr static std::nullptr_t ExportedFn = nullptr; + + /** + * Move a value from the source register to the destination. + */ + void export_move(int32_t destination, int32_t source) + { + register_write(destination, register_read(source)); + } + + template<> + constexpr static auto ExportedFn = export_move; + + /** + * Load a capability into the destination register. + */ + void export_load(int32_t destination, int32_t source, int32_t offset) + { + Capability s{static_cast(register_read(source))}; + s.address() += offset; + register_write(destination, *s); + } + + template<> + constexpr static auto ExportedFn = export_load; + + /** + * Load and return an integer. + */ + int32_t export_load_int(int32_t source, int32_t offset) + { + Capability s{static_cast(register_read(source))}; + s.address() += offset; + return *s; + } + + template<> + constexpr static auto ExportedFn = export_load_int; + + /** + * Store a capability from a register at a specified location. + */ + void export_store(int32_t value, int32_t source, int32_t offset) + { + Capability s{static_cast(register_read(source))}; + s.address() += offset; + *s = register_read(value); + } + + template<> + constexpr static auto ExportedFn = export_store; + + /** + * Returns the address of the capability in the specified register. + */ + int32_t export_get_address(int32_t regno) + { + Capability value{register_read(regno)}; + return value.address(); + } + + template<> + constexpr static auto ExportedFn = export_get_address; + + /** + * Set the address of the capability in the specified register. + */ + void export_set_address(int32_t regno, int32_t address) + { + Capability value{register_read(regno)}; + value.address() = address; + register_write(regno, value); + } + + template<> + constexpr static auto ExportedFn = export_set_address; + + /** + * Return the base address of the capability in the specified register. + */ + int32_t export_get_base(int32_t regno) + { + Capability value{register_read(regno)}; + return value.base(); + } + + template<> + constexpr static auto ExportedFn = export_get_base; + + /** + * Return the length of the capability in the specified register. + */ + int32_t export_get_length(int32_t regno) + { + Capability value{register_read(regno)}; + return value.length(); + } + + template<> + constexpr static auto ExportedFn = export_get_length; + + /** + * Return the permissions of the capability in the specified register. + */ + int32_t export_get_permissions(int32_t regno) + { + Capability value{register_read(regno)}; + return value.permissions().as_raw(); + } + + template<> + constexpr static auto ExportedFn = export_get_permissions; + + auto *gpio_device() + { +#if defined(SUNBURST) && DEVICE_EXISTS(gpio_board) + return MMIO_CAPABILITY(SonataGPIO, gpio_board); +#else +# error "No GPIO device" + return nullptr; +#endif + } + + /** + * Turn an LED on. + */ + void export_led_on(int32_t index) + { + gpio_device()->led_on(index); + } + + template<> + constexpr static auto ExportedFn = export_led_on; + + /** + * Turn an LED off. + */ + void export_led_off(int32_t index) + { + gpio_device()->led_off(index); + } + + template<> + constexpr static auto ExportedFn = export_led_off; + + /** + * Read a single button. + */ + int32_t export_read_button(int32_t index) + { +#if HAS_BUTTONS + return gpio_device()->button(index); +#else + return 0; +#endif + } + + template<> + constexpr static auto ExportedFn = export_read_button; + + /** + * Read a single switch. + */ + int32_t export_read_switch(int32_t index) + { +#if HAS_SWITCHES + return gpio_device()->switch_value(index); +#else + return 0; +#endif + } + + template<> + constexpr static auto ExportedFn = export_read_switch; + + /** + * Read all buttons. + */ + int32_t export_read_buttons() + { +#if HAS_BUTTONS + return gpio_device()->buttons(); +#else + return 0; +#endif + } + + template<> + constexpr static auto ExportedFn = export_read_buttons; + + /** + * Read all switches. + */ + int32_t export_read_switches() + { +#if HAS_SWITCHES + return gpio_device()->switches(); +#else + return 0; +#endif + } + + template<> + constexpr static auto ExportedFn = export_read_switches; + + /** + * Turn an LED on. + */ + void export_led_set(int32_t index, bool state) + { + auto gpio = gpio_device(); + if (state) + { + gpio->led_on(index); + } + else + { + gpio->led_off(index); + } + } + + template<> + constexpr static auto ExportedFn = export_led_set; + +#include "userffi.h" + + template<> + constexpr static auto ExportedFn = read_from_snapshot; + + /** + * Base template for exported functions. Forwards to the function defined + * with `ExportedFn`. + */ + template + mvm_TeError exported_function(mvm_VM *vm, + mvm_HostFunctionID, + mvm_Value *result, + mvm_Value *args, + uint8_t argCount) + { + return call_export>(vm, result, args, argCount); + } + + /** + * Print a string passed from JavaScript. + */ + template<> + mvm_TeError exported_function(mvm_VM *vm, + mvm_HostFunctionID funcID, + mvm_Value *result, + mvm_Value *args, + uint8_t argCount) + { + // Helper to write a C string to the UART. + auto puts = [](const char *str) { + while (char c = *(str++)) + { + MMIO_CAPABILITY(Uart, uart)->blocking_write(c); + } + }; + puts("\033[32;1m"); + // Iterate over the arguments. + for (unsigned i = 0; i < argCount; i++) + { + // Coerce the argument to a string and get it as a C string and + // write it to the UART. + puts(mvm_toStringUtf8(vm, args[i], nullptr)); + } + // Write a trailing newline + puts("\033[0m\n"); + // Unconditionally return success + return MVM_E_SUCCESS; + } + + /** + * Write a string passed from JavaScript to UART1 + */ + template<> + mvm_TeError exported_function(mvm_VM *vm, + mvm_HostFunctionID funcID, + mvm_Value *result, + mvm_Value *args, + uint8_t argCount) + { + // Helper to write a C string to the UART. + auto puts = [](const char *str) { + while (char c = *(str++)) + { + MMIO_CAPABILITY(Uart, uart1)->blocking_write(c); + } + }; + // Iterate over the arguments. + for (unsigned i = 0; i < argCount; i++) + { + // Coerce the argument to a string and get it as a C string and + // write it to the UART. + puts(mvm_toStringUtf8(vm, args[i], nullptr)); + } + // Unconditionally return success + return MVM_E_SUCCESS; + } + + /** + * Crash the network stack + */ + bool export_network_fault_inject() + { +#if CHERIOT_RTOS_OPTION_NETWORK_INJECT_FAULTS + network_inject_fault(); + return true; +#endif + return false; + } + + template<> + constexpr static auto ExportedFn = + export_network_fault_inject; + + /** + * Callback from microvium that resolves imports. + * + * This resolves each function to the template instantiation of + * `exported_function` with `funcID` as the template parameter. + */ + mvm_TeError + resolve_import(mvm_HostFunctionID funcID, void *, mvm_TfHostFunction *out) + { + return magic_enum::enum_switch( + [&](auto val) { + constexpr Exports Export = val; + *out = exported_function; + return MVM_E_SUCCESS; + }, + Exports(funcID), + MVM_E_UNRESOLVED_IMPORT); + } + + /** + * Helper that deletes a Microvium VM when used with a C++ unique pointer. + */ + struct MVMDeleter + { + void operator()(mvm_VM *mvm) const + { + mvm_free(mvm); + } + }; +} // namespace diff --git a/smartmeter/cheriot/mosquitto.org.h b/smartmeter/cheriot/mosquitto.org.h new file mode 100644 index 0000000..2d0018a --- /dev/null +++ b/smartmeter/cheriot/mosquitto.org.h @@ -0,0 +1,52 @@ +// Generated by brssl ta from the mosquitto.org.crt certificate + +static const unsigned char TA0_DN[] = { + 0x30, 0x81, 0x90, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, + 0x02, 0x47, 0x42, 0x31, 0x17, 0x30, 0x15, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0C, + 0x0E, 0x55, 0x6E, 0x69, 0x74, 0x65, 0x64, 0x20, 0x4B, 0x69, 0x6E, 0x67, 0x64, + 0x6F, 0x6D, 0x31, 0x0E, 0x30, 0x0C, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C, 0x05, + 0x44, 0x65, 0x72, 0x62, 0x79, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, + 0x0A, 0x0C, 0x09, 0x4D, 0x6F, 0x73, 0x71, 0x75, 0x69, 0x74, 0x74, 0x6F, 0x31, + 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x0B, 0x0C, 0x02, 0x43, 0x41, 0x31, + 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x0D, 0x6D, 0x6F, 0x73, + 0x71, 0x75, 0x69, 0x74, 0x74, 0x6F, 0x2E, 0x6F, 0x72, 0x67, 0x31, 0x1F, 0x30, + 0x1D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x01, 0x16, + 0x10, 0x72, 0x6F, 0x67, 0x65, 0x72, 0x40, 0x61, 0x74, 0x63, 0x68, 0x6F, 0x6F, + 0x2E, 0x6F, 0x72, 0x67}; + +static const unsigned char TA0_RSA_N[] = { + 0xC1, 0x34, 0x1C, 0xA9, 0x88, 0xCD, 0xF4, 0xCE, 0xC2, 0x42, 0x8B, 0x4F, 0x74, + 0xC7, 0x1D, 0xEF, 0x8E, 0x6D, 0xD8, 0xB3, 0x6A, 0x63, 0xE0, 0x51, 0x99, 0x83, + 0xEB, 0x84, 0xDF, 0xDF, 0x32, 0x5D, 0x35, 0xE6, 0x06, 0x62, 0x7E, 0x02, 0x11, + 0x76, 0xF2, 0x3F, 0xA7, 0xF2, 0xDE, 0xD5, 0x9C, 0xF1, 0x2D, 0x9B, 0xA1, 0x6E, + 0x9D, 0xCE, 0xB1, 0xFC, 0x49, 0xD1, 0x5F, 0xF6, 0xEA, 0x37, 0xDB, 0x41, 0x89, + 0x03, 0xD0, 0x7B, 0x53, 0x51, 0x56, 0x4D, 0xED, 0xF1, 0x75, 0xAF, 0xCB, 0x9B, + 0x72, 0x45, 0x7D, 0xA1, 0xE3, 0x91, 0x6C, 0x3B, 0x8C, 0x1C, 0x1C, 0x6A, 0xE4, + 0x19, 0x8E, 0x91, 0x88, 0x34, 0x76, 0xA9, 0x1D, 0x19, 0x69, 0x88, 0x26, 0x6C, + 0xAA, 0xE0, 0x2D, 0x84, 0xE8, 0x31, 0x5B, 0xD4, 0xA0, 0x0E, 0x06, 0x25, 0x1B, + 0x31, 0x00, 0xB3, 0x4E, 0xA9, 0x90, 0x41, 0x62, 0x33, 0x0F, 0xAA, 0x0D, 0xF2, + 0xE8, 0xFE, 0xCC, 0x45, 0x28, 0x1E, 0xAF, 0x42, 0x51, 0x5E, 0x90, 0xC7, 0x82, + 0xCA, 0x68, 0xCB, 0x09, 0xB3, 0x70, 0x3C, 0x9C, 0xAA, 0xCA, 0x11, 0x66, 0x3D, + 0x6C, 0x22, 0xA3, 0xF3, 0xC3, 0x32, 0xBB, 0x81, 0x4F, 0x33, 0xC7, 0xDD, 0xC8, + 0xA8, 0x06, 0x7A, 0xC9, 0x58, 0xA5, 0xDC, 0xDC, 0xE8, 0xD7, 0x74, 0xB1, 0x85, + 0x24, 0xE7, 0xE3, 0xEE, 0x93, 0xF4, 0x8F, 0xF7, 0x6B, 0xD8, 0xB1, 0xFB, 0xD9, + 0xE4, 0xAF, 0xBF, 0x73, 0xD0, 0x40, 0x59, 0x7D, 0xD0, 0x26, 0x4F, 0x16, 0x1A, + 0xC2, 0x51, 0xC4, 0x47, 0x49, 0x2C, 0x68, 0x13, 0xAC, 0xA3, 0x18, 0xE7, 0x67, + 0xCF, 0xB7, 0xFA, 0x3E, 0xF7, 0x8B, 0x20, 0x1E, 0x7B, 0xE2, 0x44, 0x0E, 0x47, + 0x0B, 0x7C, 0x78, 0xF9, 0xF4, 0xCA, 0x27, 0x6B, 0x4C, 0x2D, 0x62, 0x72, 0xD8, + 0xA4, 0x10, 0x3D, 0xE7, 0x1D, 0x88, 0x4C, 0x50, 0xE5}; + +static const unsigned char TA0_RSA_E[] = {0x01, 0x00, 0x01}; + +static const br_x509_trust_anchor TAs[1] = { + {{(unsigned char *)TA0_DN, sizeof TA0_DN}, + BR_X509_TA_CA, + {BR_KEYTYPE_RSA, + {.rsa = { + (unsigned char *)TA0_RSA_N, + sizeof TA0_RSA_N, + (unsigned char *)TA0_RSA_E, + sizeof TA0_RSA_E, + }}}}}; + +#define TAs_NUM 1 diff --git a/smartmeter/cheriot/provider.cc b/smartmeter/cheriot/provider.cc new file mode 100644 index 0000000..e7112cf --- /dev/null +++ b/smartmeter/cheriot/provider.cc @@ -0,0 +1,330 @@ +// Copyright SCI Semiconductor and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +/* + * The provider compartment. + * + * Maintains a MQTT connection and reports sensor data + */ + +#define CHERIOT_NO_AMBIENT_MALLOC +#include "common.hh" + +#include +#include +#include +#include +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY +# include +#endif +#include +#include +#include +#include +#include + +#include MQTT_BROKER_ANCHOR + +DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(ProviderMallocCapability, 24 * 1024); +#define MALLOC_CAPABILITY STATIC_SEALED_VALUE(ProviderMallocCapability) + +using CHERI::Capability; + +using Debug = ConditionalDebug; + +/// Maximum permitted MQTT client identifier length (from the MQTT +/// specification) +constexpr size_t MQTTMaximumClientLength = 23; +/// Prefix for MQTT client identifier +constexpr std::string_view clientIDPrefix{"cheriotsmp"}; +/// Space for the random client ID. +static std::array + clientID; +static_assert(clientID.size() <= MQTTMaximumClientLength); + +// MQTT network buffer sizes +constexpr const size_t networkBufferSize = 1024; +constexpr const size_t incomingPublishCount = 2; +constexpr const size_t outgoingPublishCount = 2; + +DECLARE_AND_DEFINE_CONNECTION_CAPABILITY(MQTTConnectionRightsProvider, + MQTT_BROKER_HOST, + 8883, + ConnectionTypeTCP); + +constexpr std::string_view scheduleTopicPrefix{ + "cheriot-smartmeter/p/schedule/"}; +static std::array + scheduleTopic; +constexpr std::string_view varianceTopicPrefix{ + "cheriot-smartmeter/p/variance/"}; +static std::array + varianceTopic; +constexpr std::string_view publishTopicPrefix{"cheriot-smartmeter/p/update/"}; +static std::array + publishTopic; + +static void __cheri_callback publishCallback(const char *topicName, + size_t topicNameLength, + const void *payload, + size_t payloadLength) +{ + // Check input pointers (can be skipped if the MQTT library is trusted) + Timeout t{MS_TO_TICKS(5000)}; + if (heap_claim_ephemeral(&t, topicName) != 0 || + !CHERI::check_pointer(topicName, topicNameLength)) + { + Debug::log( + "Cannot claim or verify PUBLISH callback topic name pointer."); + return; + } + + if (heap_claim_ephemeral(&t, payload) != 0 || + !CHERI::check_pointer(payload, payloadLength)) + { + Debug::log("Cannot claim or verify PUBLISH callback payload pointer."); + return; + } + + auto topicView = std::string_view{topicName, topicNameLength}; + auto payloadView = + std::string_view{static_cast(payload), payloadLength}; + + if (topicView == + std::string_view{scheduleTopic.data(), scheduleTopic.size()}) + { + /* + * 10 digits for 32-bit timestamp_day, space, 48 space-separated signed + * 16-bit numbers (5 digits, 1 sign), and a NUL byte + */ + char buf[10 + 1 + 48 * (6 + 1) + 1]; + + Debug::log("Got schedule PUBLISH: {}", payloadView); + + if (payloadLength >= sizeof(buf)) + { + Debug::log("Overlong outage PUBLISH, discarding"); + } + memcpy(buf, payload, payloadLength); + buf[payloadLength] = '\0'; + + struct provider_schedule_payload payload; + char *ptr; + payload.timestamp_day = strtoul(buf, &ptr, 10); + for (int i = 0; i < 48; i++) + { + payload.rate[i] = strtol(ptr, &ptr, 10); + } + + auto providerSchedule = SHARED_OBJECT_WITH_PERMISSIONS( + provider_schedule, provider_schedule, true, true, false, false); + providerSchedule->write(payload); + } + else if (topicView == + std::string_view{varianceTopic.data(), varianceTopic.size()}) + { + /* + * 10 digits for 32-bit timestamp_base, space, 4 space-separated signed + * 16-bit numbers (5 digits, 1 sign), 2 space-separated 16-bit unsigned + * numbers (5 digits, no sign), and a NUL byte. + */ + char buf[10 + 1 + 4 * (6 + 1) + 2 * (5 + 1) + 1]; + + Debug::log("Got variance PUBLISH: {}", payloadView); + + if (payloadLength >= sizeof(buf)) + { + Debug::log("Overlong outage PUBLISH, discarding"); + } + memcpy(buf, payload, payloadLength); + buf[payloadLength] = '\0'; + + struct provider_variance_payload payload; + char *ptr; + payload.timestamp_base = strtoul(buf, &ptr, 10); + for (int i = 0; i < 2; i++) + { + payload.start[i] = strtol(ptr, &ptr, 10); + payload.duration[i] = strtoul(ptr, &ptr, 10); + payload.rate[i] = strtol(ptr, &ptr, 10); + } + + auto providerVariance = SHARED_OBJECT_WITH_PERMISSIONS( + provider_variance, provider_variance, true, true, false, false); + providerVariance->write(payload); + } + else + { + Debug::log("Unknown topic in PUBLISH callback: {}", topicView); + } +} + +int provider_entry() +{ + int ret; + Timeout noTimeout{UnlimitedTimeout}; + + Debug::log("entry"); + housekeeping_initial_barrier(); + Debug::log("initialization barrier down"); + + const char *mqttName = housekeeping_mqtt_unique_get(); + HOUSEKEEPING_MQTT_CONCAT(clientID, clientIDPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(publishTopic, publishTopicPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(scheduleTopic, scheduleTopicPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(varianceTopic, varianceTopicPrefix, mqttName); + + Debug::log("Listening on {}", + std::string_view{scheduleTopic.data(), scheduleTopic.size()}); + + while (true) + { + int tick = 0; + + Debug::log("Connecting to MQTT broker..."); + + Timeout connectTimeout{MS_TO_TICKS(30000)}; + + MQTTConnection handle = + mqtt_connect(&connectTimeout, + MALLOC_CAPABILITY, + CONNECTION_CAPABILITY(MQTTConnectionRightsProvider), + publishCallback, + nullptr /* XXX should watch our ACK stream */, + TAs, + TAs_NUM, + networkBufferSize, + incomingPublishCount, + outgoingPublishCount, + clientID.data(), + clientID.size()); + + if (!Capability{handle}.is_valid()) + { + Debug::log("Failed to connect."); + goto retry; + } + + Debug::log("Connected to MQTT broker!"); + + ret = mqtt_subscribe(&connectTimeout, + handle, + 1, // QoS 1 = delivered at least once + scheduleTopic.data(), + scheduleTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe for outages: {}", ret); + goto retry; + } + + // XXX clobbers packet ID; we should be watching our ACK stream + ret = mqtt_subscribe(&connectTimeout, + handle, + 1, // QoS 1 = delivered at least once + varianceTopic.data(), + varianceTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe for requests: {}", ret); + goto retry; + } + + { +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY + auto sensorDataCoarse = + SHARED_OBJECT_WITH_PERMISSIONS(sensor_data_coarse, + sensor_data_coarse, + true, + false, + false, + false); +#else + auto sensorDataCoarse = &theData.sensor_data_coarse; +#endif + + sensor_data_coarse localSensorData = {0}; + + Timeout loopTimeout{MS_TO_TICKS(5000)}; + while (true) + { + ret = mqtt_run(&loopTimeout, handle); + + if (ret < 0) + { + Debug::log("Failed to run MQTT, error {}; hanging up", ret); + goto retry; + } + else if (loopTimeout.remaining == 0) + { + /* + * XXX We'd love to be multi-waiting on the network stack + * and the sensorData->timestamp, but the APIs we have make + * that harder than it should be. + */ + + Timeout readTimeout{MS_TO_TICKS(1000)}; + ret = sensorDataCoarse->read(&readTimeout, + localSensorData.version, + localSensorData.payload); + if (ret == 0) + { + Debug::log("Awake and publishing {}", + localSensorData.version); + + /* + * 10 digits for 3 32-bit values, space separated, + * with NUL terminator: the timestamp and two most + * recent values. + */ + char msg[(10 + 1) * 3]; + ssize_t msglen = + snprintf(msg, + sizeof(msg), + "%d %d %d", + localSensorData.payload.timestamp, + localSensorData.payload.samples[0], + localSensorData.payload.samples[1]); + + Timeout t{MS_TO_TICKS(5000)}; + ret = mqtt_publish(&t, + handle, + 1, // QoS 1 = delivered at least once + publishTopic.data(), + publishTopic.size(), + msg, + msglen); + + if (ret < 0) + { + Debug::log("Failed to publish, error {}.", ret); + goto retry; + } + } + else + { + Debug::log("Awake but skipping publish: {}", ret); + } + + loopTimeout = Timeout{MS_TO_TICKS(5000)}; + } + } + } + + retry: + if (Capability{handle}.is_valid()) + { + mqtt_disconnect(&noTimeout, MALLOC_CAPABILITY, handle); + } + + Timeout t{MS_TO_TICKS(5000)}; + thread_sleep(&t, ThreadSleepNoEarlyWake); + } + + return 0; +} diff --git a/smartmeter/cheriot/sensor.cc b/smartmeter/cheriot/sensor.cc new file mode 100644 index 0000000..2a2b184 --- /dev/null +++ b/smartmeter/cheriot/sensor.cc @@ -0,0 +1,206 @@ +// Copyright SCI Semiconductor and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +/** + * The sensor compartment. + * + * Responsible for reading power consumption (or a simulation thereof) and + * calling callbacks in the other compartments. + */ + +#include "common.hh" + +#include +#include +#include +#include +#include +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY +# include +#endif +#ifdef SMARTMETER_FAKE_UARTLESS_SENSOR +# include +#endif + +#ifdef MONOLITH_BUILD_WITHOUT_SECURITY +struct merged_data theData; + +struct merged_data *monolith_merged_data_get() +{ + return &theData; +} +#endif + +using Debug = ConditionalDebug; + +DECLARE_AND_DEFINE_INTERRUPT_CAPABILITY(uart1InterruptCap, + Uart1Interrupt, + true, + true); +const uint32_t *uart1InterruptFutex; + +#ifndef SMARTMETER_FAKE_UARTLESS_SENSOR +static constexpr size_t MaximumLineLength = 64; + +/** + * Read line from uart, discarding any over-long line. + */ +void read_line(std::string &ret) +{ + ret.clear(); + + /* + * If we've overrun the maximum length, then we're dropping bytes until we + * find a newline, at which point we'll pick up again. + */ + bool discarding = false; + + auto uart = MMIO_CAPABILITY(Uart, uart1); + + while (true) + { + Timeout t{MS_TO_TICKS(60000)}; + auto irqCount = *uart1InterruptFutex; + + while ((uart->status & OpenTitanUart::StatusReceiveEmpty) == 0) + { + char c = uart->readData; + if (c == '\n') + { + if (!discarding) + { + return; + } + else + { + Debug::log("reset"); + discarding = false; + } + } + else if (ret.size() == MaximumLineLength) + { + Debug::log("overlong"); + discarding = true; + ret.clear(); + } + else if (!discarding) + { + ret.push_back(c); + } + } + + interrupt_complete(STATIC_SEALED_VALUE(uart1InterruptCap)); + + auto waitRes = futex_timed_wait(&t, uart1InterruptFutex, irqCount); + if (waitRes != 0) + { + Debug::log("Unexpected wait return {}", waitRes); + } + } +} +#endif + +int sensor_entry() +{ + uart1InterruptFutex = + interrupt_futex_get(STATIC_SEALED_VALUE(uart1InterruptCap)); + + housekeeping_initial_barrier(); + Debug::log("initialization barrier down"); + +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY + auto sensorDataFine = SHARED_OBJECT_WITH_PERMISSIONS( + sensor_data_fine, sensor_data_fine, true, true, false, false); + + auto sensorDataCoarse = SHARED_OBJECT_WITH_PERMISSIONS( + sensor_data_coarse, sensor_data_coarse, true, true, false, false); +#else + auto sensorDataFine = &theData.sensor_data_fine; + auto sensorDataCoarse = &theData.sensor_data_coarse; +#endif + + struct sensor_data_coarse_payload nextCoarsePayload = {0}; + bool sendNextCoarsePayload = false; + int coarseSampleCount = 0; + int32_t coarseSampleAccumulator = 0; + +#ifdef SMARTMETER_FAKE_UARTLESS_SENSOR + ds::xoroshiro::P32R16 rand = {}; + int sampleBase = 0; +#else + std::string line; + + line.reserve(MaximumLineLength); + Debug::Assert(line.capacity() >= MaximumLineLength, + "read_line alloc failed: {}", + line.capacity()); +#endif + + while (1) + { + int sample = 0; + +#ifdef SMARTMETER_FAKE_UARTLESS_SENSOR + int sampleDelta = rand.next() % 16; + if (sampleDelta != 0) + { + sampleBase += sampleDelta - 8; + } + sample = sampleBase; +#else + read_line(line); + Debug::log("Got line {}", line); + if (line.starts_with("powerSample")) + { + sample = + strtol(line.substr(sizeof("powerSample")).c_str(), nullptr, 0); + Debug::log("Sample {}", sample); + } +#endif + + coarseSampleAccumulator += sample; + + timeval tv; + int ret = gettimeofday(&tv, nullptr); + if (ret == 0) + { + struct sensor_data_fine_payload nextFinePayload = {0}; + nextFinePayload.timestamp = tv.tv_sec; + nextFinePayload.samples[0] = sample; + memcpy(&nextFinePayload.samples[1], + &sensorDataFine->payload.samples[0], + sizeof(nextFinePayload.samples) - + sizeof(nextFinePayload.samples[0])); + + sensorDataFine->write(nextFinePayload); + + if (sendNextCoarsePayload) + { + nextCoarsePayload.timestamp = tv.tv_sec; + sensorDataCoarse->write(nextCoarsePayload); + sendNextCoarsePayload = false; + } + } + + if (coarseSampleCount++ == SENSOR_COARSENING) + { + memmove(&nextCoarsePayload.samples[1], + &nextCoarsePayload.samples[0], + sizeof(nextCoarsePayload.samples) - + sizeof(nextCoarsePayload.samples[0])); + + nextCoarsePayload.samples[0] = coarseSampleAccumulator; + coarseSampleAccumulator = 0; + + coarseSampleCount = 0; + sendNextCoarsePayload = true; + } + + Debug::log("Tick {}...", tv.tv_sec); + +#ifdef SMARTMETER_FAKE_UARTLESS_SENSOR + Timeout t{MS_TO_TICKS(1000)}; + thread_sleep(&t, ThreadSleepNoEarlyWake); +#endif + } +} diff --git a/smartmeter/cheriot/user.cc b/smartmeter/cheriot/user.cc new file mode 100644 index 0000000..3be086b --- /dev/null +++ b/smartmeter/cheriot/user.cc @@ -0,0 +1,338 @@ +// Copyright Microsoft and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#define CHERIOT_NO_AMBIENT_MALLOC + +#include "common.hh" +#include +#include +#include +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY +# include +#endif +#include +#include +#include +#include +#include + +#include MQTT_BROKER_ANCHOR + +DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(UserMallocCapability, 24 * 1024); +#define MALLOC_CAPABILITY STATIC_SEALED_VALUE(UserMallocCapability) + +/// Expose debugging features unconditionally for this compartment. +using Debug = ConditionalDebug; +using CHERI::Capability; + +static uint32_t net_wake_count; +static uint32_t timebase_zero; +static uint16_t timebase_rate = 1; + +/// Thread entry point. +int user_data_entry() +{ + Debug::log("data entry"); + + static constexpr size_t nEvents = 6; + +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY + auto sensorDataFine = SHARED_OBJECT_WITH_PERMISSIONS( + sensor_data_fine, sensor_data_fine, true, false, false, false); +#else + auto sensorDataFine = &monolith_merged_data_get()->sensor_data_fine; +#endif + + Debug::log("sensor structure is {}", + reinterpret_cast(sensorDataFine)); + + auto gridOutage = SHARED_OBJECT_WITH_PERMISSIONS( + grid_planned_outage, grid_planned_outage, true, false, false, false); + auto gridRequest = SHARED_OBJECT_WITH_PERMISSIONS( + grid_request, grid_request, true, false, false, false); + auto providerSchedule = SHARED_OBJECT_WITH_PERMISSIONS( + provider_schedule, provider_schedule, true, false, false, false); + auto providerVariance = SHARED_OBJECT_WITH_PERMISSIONS( + provider_variance, provider_variance, true, false, false, false); + + auto userCrashCount = SHARED_OBJECT_WITH_PERMISSIONS( + user_crash_count, user_crash_count, true, true, false, false); + + MultiWaiter mw; + struct EventWaiterSource events[nEvents] = {{&sensorDataFine->version, 0}, + {&gridOutage->version, 0}, + {&gridRequest->version, 0}, + {&providerSchedule->version, 0}, + {&providerVariance->version, 0}, + {&net_wake_count, 0}}; + + { + int ret = + blocking_forever(MALLOC_CAPABILITY, &mw, nEvents); + Debug::Invariant(ret == 0, "Could not create multiwaiter object"); + } + + uint32_t event_futex_cache[nEvents] = {0}; + +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY + auto snapshots = SHARED_OBJECT_WITH_PERMISSIONS( + userjs_snapshot, userJS_snapshot, true, true, false, false); +#else + auto *snapshots = &monolith_merged_data_get()->userjs_snapshot; +#endif + + Debug::log("snapshot structure is {}", reinterpret_cast(snapshots)); + + while (true) + { + for (size_t i = 0; i < nEvents; i++) + { + events[i].value = event_futex_cache[i]; + } + + int ret = blocking_forever(mw, events, nEvents); + Debug::Invariant(ret == 0, "Failed to wait for events: {}", ret); + + userjs_snapshot localSnapshots; + + // Update snapshots + // TODO: limited timeouts + // TODO: collect which ones were updated to pass to JS + + Timeout t{UnlimitedTimeout}; + if (sensorDataFine->read( + &t, event_futex_cache[0], localSnapshots.sensor_data) == 0) + { + /* Recompute our copy according to our timebase zero and rate */ + uint32_t &sts = localSnapshots.sensor_data.timestamp; + if (sts >= timebase_zero) + { + uint32_t elapsed = sts - timebase_zero; + elapsed *= timebase_rate; + sts = timebase_zero + elapsed; + } + } + gridOutage->read(&t, event_futex_cache[1], localSnapshots.grid_outage); + gridRequest->read( + &t, event_futex_cache[2], localSnapshots.grid_request); + providerSchedule->read( + &t, event_futex_cache[3], localSnapshots.provider_schedule); + providerVariance->read( + &t, event_futex_cache[4], localSnapshots.provider_variance); + + event_futex_cache[5] = net_wake_count; + + *snapshots = localSnapshots; + + ret = user_javascript_run(); + if (ret == -ECOMPARTMENTFAIL) + { + Debug::log("JavaScript compartment crashed during tick"); + user_crash_count_payload newPayload{ + userCrashCount->payload.crashes_since_boot + 1}; + userCrashCount->write(newPayload); + } + } + + return 0; +} + +/// Maximum permitted MQTT client identifier length (from the MQTT +/// specification) +constexpr size_t MQTTMaximumClientLength = 23; +/// Prefix for MQTT client identifier +constexpr std::string_view clientIDPrefix{"cheriotsmu"}; +/// Space for the random client ID. +static std::array + clientID; +static_assert(clientID.size() <= MQTTMaximumClientLength); + +// MQTT network buffer sizes +constexpr const size_t networkBufferSize = 4096; +constexpr const size_t incomingPublishCount = 2; +constexpr const size_t outgoingPublishCount = 2; + +DECLARE_AND_DEFINE_CONNECTION_CAPABILITY(MQTTConnectionRightsUser, + MQTT_BROKER_HOST, + 8883, + ConnectionTypeTCP); + +constexpr std::string_view jsTopicPrefix{"cheriot-smartmeter/u/js/"}; +std::array jsTopic; + +constexpr std::string_view timebaseTopicPrefix{ + "cheriot-smartmeter/u/timebase/"}; +std::array + timebaseTopic; + +static void __cheri_callback publishCallback(const char *topicName, + size_t topicNameLength, + const void *payload, + size_t payloadLength) +{ + // Check input pointers (can be skipped if the MQTT library is trusted) + Timeout t{MS_TO_TICKS(5000)}; + if (heap_claim_ephemeral(&t, topicName) != 0 || + !CHERI::check_pointer(topicName, topicNameLength)) + { + Debug::log( + "Cannot claim or verify PUBLISH callback topic name pointer."); + return; + } + + if (heap_claim_ephemeral(&t, payload) != 0 || + !CHERI::check_pointer(payload, payloadLength)) + { + Debug::log("Cannot claim or verify PUBLISH callback payload pointer."); + return; + } + + auto topicView = std::string_view{topicName, topicNameLength}; + + if (topicView == std::string_view{jsTopic.data(), jsTopic.size()}) + { + void *newPayload = blocking_forever( + MALLOC_CAPABILITY, payloadLength, AllocateWaitRevocationNeeded); + + if (!Capability{newPayload}.is_valid()) + { + Debug::log("No memory for new JS; that's sad. {}", payloadLength); + return; + } + + memcpy(newPayload, payload, payloadLength); + user_javascript_load(static_cast(newPayload), + payloadLength); + + // The other compartment has claimed it; drop our claim. + heap_free(MALLOC_CAPABILITY, newPayload); + + net_wake_count++; + futex_wake(&net_wake_count, UINT32_MAX); + } + else if (topicView == + std::string_view{timebaseTopic.data(), timebaseTopic.size()}) + { + /* + * 10 digits for 32-bit timebase_zero, space, 5 digits for unsigned + * 16-bit number, NUL + */ + char buf[10 + 1 + 5 + 1]; + + if (payloadLength >= sizeof(buf)) + { + Debug::log("Overlong timebase PUBLISH, discarding"); + return; + } + memcpy(buf, payload, payloadLength); + buf[payloadLength] = '\0'; + + char *ptr; + timebase_zero = strtoul(buf, &ptr, 10); + timebase_rate = strtoul(ptr, &ptr, 10); + + Debug::log("New timebase {} {}; re-evaulating policy", + timebase_zero, + timebase_rate); + + net_wake_count++; + futex_wake(&net_wake_count, UINT32_MAX); + } + else + { + Debug::log("Unknown topic in PUBLISH callback: {}", topicView); + } +} + +/// Thread entry point. +int user_net_entry() +{ + int ret; + Timeout noTimeout{UnlimitedTimeout}; + + Debug::log("net entry"); + housekeeping_initial_barrier(); + Debug::log("initialization barrier down"); + + const char *mqttName = housekeeping_mqtt_unique_get(); + HOUSEKEEPING_MQTT_CONCAT(clientID, clientIDPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(jsTopic, jsTopicPrefix, mqttName); + HOUSEKEEPING_MQTT_CONCAT(timebaseTopic, timebaseTopicPrefix, mqttName); + + while (true) + { + int tick = 0; + + Debug::log("Connecting to MQTT broker..."); + + Timeout connectTimeout{MS_TO_TICKS(30000)}; + + MQTTConnection handle = + mqtt_connect(&connectTimeout, + MALLOC_CAPABILITY, + CONNECTION_CAPABILITY(MQTTConnectionRightsUser), + publishCallback, + nullptr /* XXX should watch our ACK stream */, + TAs, + TAs_NUM, + networkBufferSize, + incomingPublishCount, + outgoingPublishCount, + clientID.data(), + clientID.size()); + + if (!Capability{handle}.is_valid()) + { + Debug::log("Failed to connect."); + goto retry; + } + + Debug::log("Connected to MQTT broker!"); + + ret = mqtt_subscribe(&connectTimeout, + handle, + 1, // QoS 1 = delivered at least once + jsTopic.data(), + jsTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe for new code: {}", ret); + goto retry; + } + + ret = mqtt_subscribe(&connectTimeout, + handle, + 1, // QoS 1 = delivered at least once + timebaseTopic.data(), + timebaseTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe for timebase: {}", ret); + goto retry; + } + + while (true) + { + ret = mqtt_run(&noTimeout, handle); + + if (ret < 0) + { + Debug::log("Failed to run MQTT, error {}; hanging up", ret); + goto retry; + } + } + + retry: + if (Capability{handle}.is_valid()) + { + mqtt_disconnect(&noTimeout, MALLOC_CAPABILITY, handle); + } + + Timeout t{MS_TO_TICKS(5000)}; + thread_sleep(&t, ThreadSleepNoEarlyWake); + } + + return 0; +} diff --git a/smartmeter/cheriot/userJS.cc b/smartmeter/cheriot/userJS.cc new file mode 100644 index 0000000..fc3f52a --- /dev/null +++ b/smartmeter/cheriot/userJS.cc @@ -0,0 +1,121 @@ +#define CHERIOT_NO_AMBIENT_MALLOC +#include "common.hh" +#include "default-javascript.h" +#include "microvium-ffi.hh" +#include + +DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(JavaScriptMallocCapability, 9 * 1024); +#define JAVASCRIPT_MALLOC STATIC_SEALED_VALUE(JavaScriptMallocCapability) + +using Debug = ConditionalDebug; + +using CHERI::Capability; + +std::unique_ptr vm; +const uint8_t *claimed_bytecode; +; + +FlagLock lock; + +/* + * In monolith builds, this is _the_ compartment error handler! + */ +enum ErrorRecoveryBehaviour +compartment_error_handler(struct ErrorState *frame, size_t mcause, size_t mtval) +{ + Debug::log("Crash detected in JavaScript compartment. PCC: {}", + frame->pcc); + vm.reset(nullptr); + lock.unlock(); + return ErrorRecoveryBehaviour::ForceUnwind; +} + +static void cleanup() +{ + vm.reset(nullptr); + claimed_bytecode = nullptr; + heap_free_all(JAVASCRIPT_MALLOC); +} + +int user_javascript_run(/* XXX */) +{ + LockGuard g{lock}; + if (!vm) + { + Debug::log("No dynamic JavaScript bytecode provided, loading default " + "JavaScript"); + mvm_VM *rawVm; + int err = mvm_restore( + &rawVm, /* Out pointer to the VM */ + hello_mvm_bc, /* Bytecode data */ + hello_mvm_bc_len, /* Bytecode length */ + JAVASCRIPT_MALLOC, /* Capability used to allocate memory */ + ::resolve_import); /* Callback used to resolve FFI imports */ + // If this is not valid bytecode, give up. + Debug::Assert( + err == MVM_E_SUCCESS, "Failed to parse bytecode: {}", err); + vm.reset(rawVm); + } + mvm_Value run; + if (mvm_resolveExports(vm.get(), &ExportRun, &run, 1) == MVM_E_SUCCESS) + { + // Allocate the space for the VM capability registers on the stack and + // record its location. + // **Note**: This must be on the same stack and in same compartment as + // the JavaScript interpreter, so that the callbacks can re-derive it + // from csp. + AttackerRegisterState state; + + state_set(&state); + + // Set a limit of bytecodes to execute, to prevent infinite loops. + mvm_stopAfterNInstructions(vm.get(), 20000); + // Call the function: + int err = mvm_call(vm.get(), run, nullptr, nullptr, 0); + if (err != MVM_E_SUCCESS) + { + Debug::log("Failed to call run function: {}", err); + cleanup(); + return -EINVAL; + } + return 0; + } + return -EINVAL; +} + +int user_javascript_load(const uint8_t *bytecode, size_t size) +{ + LockGuard g{lock}; + //////////////////////////////////////////////////////////////////////// + // We've now read the bytecode into a buffer. Spin up the JavaScript + // VM to execute it. + //////////////////////////////////////////////////////////////////////// + + heap_free(JAVASCRIPT_MALLOC, const_cast(claimed_bytecode)); + + Debug::log("{} bytes of heap quota available after release", + heap_quota_remaining(JAVASCRIPT_MALLOC)); + + heap_claim(JAVASCRIPT_MALLOC, const_cast(bytecode)); + claimed_bytecode = bytecode; + + // MMIO_CAPABILITY(GPIO, gpio_led0)->enable_all(); + + mvm_TeError err; + // Create a Microvium VM from the bytecode. + { + mvm_VM *rawVm; + err = mvm_restore( + &rawVm, /* Out pointer to the VM */ + const_cast(claimed_bytecode), /* Bytecode data */ + size, /* Bytecode length */ + JAVASCRIPT_MALLOC, /* Capability used to allocate memory */ + ::resolve_import); /* Callback used to resolve FFI imports */ + // If this is not valid bytecode, give up. + Debug::Assert( + err == MVM_E_SUCCESS, "Failed to parse bytecode: {}", err); + vm.reset(rawVm); + } + + return 0; +} diff --git a/smartmeter/cheriot/userffi.h b/smartmeter/cheriot/userffi.h new file mode 100644 index 0000000..835c6bd --- /dev/null +++ b/smartmeter/cheriot/userffi.h @@ -0,0 +1,97 @@ + +// This file is generated by js/ffigen.js; do not edit. +// Regenerate with "microvium --no-snapshot ffigen.js" +// and "clang-format -i userffi.h" +#pragma once + +enum userjs_snapshot_index +{ + DATA_SENSOR_TIMESTAMP = 1, + DATA_SENSOR_SAMPLE = 2, + DATA_GRID_OUTAGE_START = 3, + DATA_GRID_OUTAGE_DURATION = 4, + DATA_GRID_REQUEST_START_TIME = 5, + DATA_GRID_REQUEST_DURATION = 6, + DATA_GRID_REQUEST_SEVERITY = 7, + DATA_PROVIDER_SCHEDULE_TIMESTAMP_DAY = 8, + DATA_PROVIDER_SCHEDULE_RATE = 9, + DATA_PROVIDER_SCHEDULE_RATE_ARRAY = 10, + DATA_PROVIDER_VARIANCE_TIMESTAMP_BASE = 11, + DATA_PROVIDER_VARIANCE_START = 12, + DATA_PROVIDER_VARIANCE_DURATION = 13, + DATA_PROVIDER_VARIANCE_RATE = 14, +}; + +int32_t read_from_snapshot(int32_t type, int32_t index) +{ +#ifndef MONOLITH_BUILD_WITHOUT_SECURITY + auto snapshots = SHARED_OBJECT_WITH_PERMISSIONS( + userjs_snapshot, userJS_snapshot, true, false, false, false); +#else + auto *snapshots = &theData.userjs_snapshot; +#endif + + switch (type) + { + case DATA_SENSOR_TIMESTAMP: + return snapshots->sensor_data.timestamp; + case DATA_SENSOR_SAMPLE: + if ((index < 0) || + (index >= (sizeof(snapshots->sensor_data.samples) / + sizeof(snapshots->sensor_data.samples[0])))) + { + return 0; + } + return snapshots->sensor_data.samples[index]; + case DATA_GRID_OUTAGE_START: + return snapshots->grid_outage.start_time; + case DATA_GRID_OUTAGE_DURATION: + return snapshots->grid_outage.duration; + case DATA_GRID_REQUEST_START_TIME: + return snapshots->grid_request.start_time; + case DATA_GRID_REQUEST_DURATION: + return snapshots->grid_request.duration; + case DATA_GRID_REQUEST_SEVERITY: + return snapshots->grid_request.severity; + case DATA_PROVIDER_SCHEDULE_TIMESTAMP_DAY: + return snapshots->provider_schedule.timestamp_day; + case DATA_PROVIDER_SCHEDULE_RATE: + if ((index < 0) || + (index >= (sizeof(snapshots->provider_schedule.rate) / + sizeof(snapshots->provider_schedule.rate[0])))) + { + return 0; + } + return snapshots->provider_schedule.rate[index]; + case DATA_PROVIDER_SCHEDULE_RATE_ARRAY: + register_write(index, &snapshots->provider_schedule.rate); + return 0; + case DATA_PROVIDER_VARIANCE_TIMESTAMP_BASE: + return snapshots->provider_variance.timestamp_base; + case DATA_PROVIDER_VARIANCE_START: + if ((index < 0) || + (index >= (sizeof(snapshots->provider_variance.start) / + sizeof(snapshots->provider_variance.start[0])))) + { + return 0; + } + return snapshots->provider_variance.start[index]; + case DATA_PROVIDER_VARIANCE_DURATION: + if ((index < 0) || + (index >= (sizeof(snapshots->provider_variance.duration) / + sizeof(snapshots->provider_variance.duration[0])))) + { + return 0; + } + return snapshots->provider_variance.duration[index]; + case DATA_PROVIDER_VARIANCE_RATE: + if ((index < 0) || + (index >= (sizeof(snapshots->provider_variance.rate) / + sizeof(snapshots->provider_variance.rate[0])))) + { + return 0; + } + return snapshots->provider_variance.rate[index]; + } + return 0; +} diff --git a/smartmeter/cheriot/xmake.lua b/smartmeter/cheriot/xmake.lua new file mode 100644 index 0000000..7bdb19d --- /dev/null +++ b/smartmeter/cheriot/xmake.lua @@ -0,0 +1,257 @@ +-- Copyright SCI Semiconductor and CHERIoT Contributors. +-- SPDX-License-Identifier: MIT + +set_project("CHERIoT MQTT Example") + +sdkdir = os.getenv("CHERIOT_RTOS_SDK") or path.absolute("../../cheriot-rtos/sdk") +includes(sdkdir) +set_toolchains("cheriot-clang") + +netdir = os.getenv("CHERIOT_NETWORK_STACK") or path.absolute("../../network-stack") +includes(path.join(netdir, "lib")) + +option("board") + set_default("sonata-1.1") + +option("broker-host") + set_default("test.mosquitto.org") + +option("broker-anchor") + set_default("mosquitto.org.h") + +option("unique-id") + set_default("random") + +option("fake-sensor") + set_default(false) + add_defines("SMARTMETER_FAKE_UARTLESS_SENSOR") + +rule("smartmeter.mqtt") + on_load(function (target) + -- Note: port 8883 to be encrypted and tolerating unautenticated connections + target:add('options', "broker-host") + local broker_host = get_config("broker-host") + target:add("defines", table.concat({"MQTT_BROKER_HOST=\"", tostring(broker_host), "\""})) + + target:add('options', "broker-anchor") + local broker_anchor = get_config("broker-anchor") + target:add("defines", table.concat({"MQTT_BROKER_ANCHOR=\"", tostring(broker_anchor), "\""})) + end) + +rule("housekeeping.unique-id") + on_load(function (target) + target:add('options', "unique-id") + local unique_id = get_config("unique-id") + target:add("defines", table.concat({"MQTT_UNIQUE_ID=\"", tostring(unique_id), "\""})) + end) + +compartment("housekeeping") + add_includedirs(path.join(netdir,"include")) + + add_files("housekeeping.cc") + + add_rules("cheriot.network-stack.ipv6", "housekeeping.unique-id") + + +compartment("sensor") + add_includedirs(path.join(netdir,"include")) + + add_files("sensor.cc") + + add_options("fake-sensor") + on_load(function(target) + target:values_set("shared_objects", + { sensor_data_coarse = 32 + , sensor_data_fine = 40 + }, {expand = false}) + end) + +compartment("grid") + add_includedirs(path.join(netdir,"include")) + + add_files("grid.cc") + + add_rules("cheriot.network-stack.ipv6", "smartmeter.mqtt") + + on_load(function(target) + target:values_set("shared_objects", + { grid_planned_outage = 12 + , grid_request = 12 }, + {expand = false}) + end) + +compartment("provider") + add_includedirs(path.join(netdir,"include")) + + add_files("provider.cc") + + add_rules("cheriot.network-stack.ipv6", "smartmeter.mqtt") + + on_load(function(target) + target:values_set("shared_objects", + { provider_schedule = 104 + , provider_variance = 20 }, + {expand = false}) + end) + +compartment("userJS") + add_includedirs(path.join(netdir,"include")) + + add_files("userJS.cc") + + local injectFaults = get_config("network-inject-faults") + add_defines("CHERIOT_RTOS_OPTION_NETWORK_INJECT_FAULTS=" .. tostring(injectFaults)) + +compartment("user") + add_includedirs(path.join(netdir,"include")) + + add_files("user.cc") + + add_rules("cheriot.network-stack.ipv6", "smartmeter.mqtt") + + on_load(function(target) + target:values_set("shared_objects", + { userJS_snapshot = 168 + , user_crash_count = 8 + }, + {expand = false}) + end) + +compartment("monolith") + add_includedirs(path.join(netdir,"include")) + + add_files("housekeeping.cc") + add_files("sensor.cc") + add_files("grid.cc") + add_files("provider.cc") + add_files("userJS.cc") + + add_options("fake-sensor") + add_rules("cheriot.network-stack.ipv6", "smartmeter.mqtt", "housekeeping.unique-id") + + local injectFaults = get_config("network-inject-faults") + add_defines("CHERIOT_RTOS_OPTION_NETWORK_INJECT_FAULTS=" .. tostring(injectFaults)) + + on_load(function(target) + target:add("defines", "MONOLITH_BUILD_WITHOUT_SECURITY") + target:values_set("shared_objects", + { grid_planned_outage = 12 + , grid_request = 12 + , provider_schedule = 104 + , provider_variance = 20 + , user_crash_count = 8 + }, + {expand = false}) + end) + +compartment("monolithUser") + add_includedirs(path.join(netdir,"include")) + + add_files("user.cc") + + add_rules("cheriot.network-stack.ipv6", "smartmeter.mqtt") + on_load(function(target) + target:add("defines", "MONOLITH_BUILD_WITHOUT_SECURITY") + end) + + +function mkthreads(overrideCompartmentName, overrideUserCompartmentName) + return { + { + -- TCP/IP stack thread. + compartment = "TCPIP", + priority = 1, + entry_point = "ip_thread_entry", + stack_size = 4096, + trusted_stack_frames = 5 + }, + { + -- Firewall thread, handles incoming packets as they arrive. + compartment = "Firewall", + -- Higher priority, this will be back-pressured by the message + -- queue if the network stack can't keep up, but we want + -- packets to arrive immediately. + priority = 2, + entry_point = "ethernet_run_driver", + stack_size = 0x1000, + trusted_stack_frames = 5 + }, + { + compartment = overrideCompartmentName or "housekeeping", + priority = 1, + entry_point = "housekeeping_entry", + stack_size = 3072, + trusted_stack_frames = 6 + }, + { + compartment = overrideCompartmentName or "sensor", + priority = 2, + entry_point = "sensor_entry", + stack_size = 2048, + trusted_stack_frames = 3 + }, + { + compartment = overrideCompartmentName or "grid", + priority = 1, + entry_point = "grid_entry", + -- TLS requires *huge* stacks! + stack_size = 8160, + trusted_stack_frames = 6 + }, + { + compartment = overrideCompartmentName or "provider", + priority = 1, + entry_point = "provider_entry", + -- TLS requires *huge* stacks! + stack_size = 8160, + trusted_stack_frames = 6 + }, + { + compartment = overrideUserCompartmentName or "user", + priority = 1, + entry_point = "user_data_entry", + stack_size = 0xa00, + trusted_stack_frames = 4 + }, + { + compartment = overrideUserCompartmentName or "user", + priority = 1, + entry_point = "user_net_entry", + -- TLS requires *huge* stacks! + stack_size = 8160, + trusted_stack_frames = 6 + } + } +end + +function mkfirmware(name, body, threads) + firmware(name) + set_policy("build.warning", true) + + -- RTOS deps + add_deps("debug", "freestanding", "microvium", "stdio", "string", "strtol") + + -- Network Stack deps + add_deps("DNS", "Firewall", "MQTT", "NetAPI", "SNTP", "TCPIP", "TLS", "time_helpers") + + add_options("tls-rsa") + on_load(function(target) + target:values_set("board", "$(board)") + target:values_set("threads", threads, {expand = false}) + end) + + body() +end + + +mkfirmware("smartmeter", + function() + add_deps("housekeeping", "sensor", "grid", "provider", "user", "userJS") + end, + mkthreads(nil --[[ many compartments --]])) + +mkfirmware("smartmeter-monolith", + function() + add_deps("monolith", "monolithUser") + end, + mkthreads("monolith", "monolithUser")) diff --git a/smartmeter/frontends/README.md b/smartmeter/frontends/README.md new file mode 100644 index 0000000..2beff19 --- /dev/null +++ b/smartmeter/frontends/README.md @@ -0,0 +1,45 @@ +## Apology + +nwf is rather naive at all things JS, so any and/or all of this might be objectively bad code. +Please do feel free to replace any and/or all of it; they hope that what's here at least serve[ds] some illustrative purpose. + +## What + +The smartmeter tenants (grid, provider, user) all want to have more convenient interfaces than the CLI. +This is a (very skeletal, at the moment) start at providing those. + +In particular, there is a NodeJS server that can turn POST requests of JavaScript code into Microvium bytecode and ship that over MQTT. + +It is also configured to serve some static files, +of which there are precious few at the moment, +but which include in-browser MQTT support and a(n again very skeletal) grid MQTT subscriber. + +Some configuration is shared between the in-browser clients and the server; see `static/democonfig.js`. + +## Building + +You'll want to ensure that you have NodeJS and NPM installed. +Running `npm install` will install our JS dependencies. + +The `CodeMirror` editor we use to allow in-browser editing of JavaScript policy programs needs to be "bundled". +Towards that end, run + + ./node_modules/.bin/rollup --config ./static.in/user-editor.rollup-config.mjs + +See https://codemirror.net/examples/bundle/ for details. + +## Running + +Then run + + node --experimental-require-module ./server.js + +For NodeJS versions v23.0.0 or later, you can elide the `--experimental-require-module`. + +You can test the Microvium compilation code, on the contents of `./foo.js`, with + + curl --verbose -H "Content-Type: text/plain" --data-binary @./foo.js http://localhost:4000/postjs/${METER_ID} + +You can see the very basic grid client by visiting http://localhost:4000/static/grid.html + +Similarly, the user view will be at http://localhost:4000/static/user-editor.html diff --git a/smartmeter/frontends/device-js/ffi.js b/smartmeter/frontends/device-js/ffi.js new file mode 120000 index 0000000..eea5765 --- /dev/null +++ b/smartmeter/frontends/device-js/ffi.js @@ -0,0 +1 @@ +../../cheriot/js/ffi.js \ No newline at end of file diff --git a/smartmeter/frontends/device-js/smartmeter.js b/smartmeter/frontends/device-js/smartmeter.js new file mode 120000 index 0000000..46adb9e --- /dev/null +++ b/smartmeter/frontends/device-js/smartmeter.js @@ -0,0 +1 @@ +../../cheriot/js/smartmeter.js \ No newline at end of file diff --git a/smartmeter/frontends/mosquitto.org.crt b/smartmeter/frontends/mosquitto.org.crt new file mode 100644 index 0000000..e76dbd8 --- /dev/null +++ b/smartmeter/frontends/mosquitto.org.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL +BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG +A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU +BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv +by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE +BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES +MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp +dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg +UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW +Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA +s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH +3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo +E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT +MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV +6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC +6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf ++pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK +sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 +LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE +m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= +-----END CERTIFICATE----- diff --git a/smartmeter/frontends/package.json b/smartmeter/frontends/package.json new file mode 100644 index 0000000..f82c0d1 --- /dev/null +++ b/smartmeter/frontends/package.json @@ -0,0 +1,24 @@ +{ + "name": "userfe", + "version": "0.0.0", + "description": "User Microvium Frontend", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "@codemirror/lang-javascript": "^6.2.3", + "@rollup/plugin-node-resolve": "^16.0.0", + "chart.js": "^4.4.8", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-annotation": "^3.1.0", + "codemirror": "^6.0.1", + "date-fns": "^4.1.0", + "express": "^4.21.2", + "microvium": "^8.0.0", + "mqtt": "^5.10.3", + "rollup": "^4.34.8" + } +} diff --git a/smartmeter/frontends/server.js b/smartmeter/frontends/server.js new file mode 100644 index 0000000..1630cf1 --- /dev/null +++ b/smartmeter/frontends/server.js @@ -0,0 +1,113 @@ +const express = require("express"); +const fs = require("fs"); +const path = require("path"); +const process = require("node:process"); +const url = require("url"); + +const Microvium = require("microvium"); +const MQTT = require("mqtt"); + +const democonfig = require("./static/democonfig.mjs"); + +const PORT = process.env.PORT || 4000; + +/// Drive Microvium through compilation +function compileMVM(sourceText) +{ + // Create an empty VM + const vm = Microvium.create(); + + // Add Microvium's default globals such as `vmExport` and `vmImport` + Microvium.addDefaultGlobals(vm); + + const importDependency = Microvium.nodeStyleImporter(vm, + { basedir: "./device-js" + }); + + try { + // Evaluate the script + vm.evaluateModule({ sourceText, debugFilename: 'firmware.js', importDependency }); + + // Create the snapshot + const snapshot = vm.createSnapshot(); + + // console.log(Microvium.decodeSnapshot(snapshot)); + + // The `snapshot.data` is a Node Buffer (Uint8Array) containing the snapshot bytes + return { "result": snapshot.data, "error": null } + } catch (e) { + return { "result": null, "error": e.message } + } +} + +// Synchronously connect to the MQTT broker +const mqttClientID = `CHERIoT${Math.random().toString(16)}`; +const mqttClient = MQTT.connect( + democonfig.MQTT_BROKER_MQTTS_URL, + { mqttClientID + , clean: true + , connectTimeout: 5000 + , reconnectPeriod: 1000 + , ca: fs.readFileSync(path.resolve("./static", democonfig.MQTT_BROKER_CERT)) + , rejectUnauthorized: democonfig.SECURE_CERT // permits self-signed certs if so configured + }); + + +// Diagnostic callbacks +mqttClient.on('connect', () => { console.log("MQTT connected"); }); +mqttClient.on('error', (error) => { console.log("MQTT Error", error) }); +mqttClient.on('reconnect', (error) => { console.log("MQTT Reconnect", error) }); + +const expressApp = express() + +// Static resources, ours and those from NPM +expressApp.use('/static', express.static('static')) +expressApp.use('/mqttjs', express.static('node_modules/mqtt/dist')) +expressApp.use('/chartjs', express.static('node_modules/chart.js/dist')) +expressApp.use('/chartjs', + express.static('node_modules/chartjs-adapter-date-fns/dist')) +expressApp.use('/chartjs', + express.static('node_modules/chartjs-plugin-annotation/dist')) + +expressApp.post('/postjs/:meter', + (req, res, next) => { console.log("POST JS", req.params.meter); next(); }, + express.text(), // XXX Probably not once we have a frontend + (req, res) => + { + const vmresult = compileMVM(req.body); + console.log(req.body, vmresult); + + if (vmresult.result === null) + { + res.status(400).send(vmresult.error); + return; + } + + if (vmresult.result.length > 4096) + { + res.status(400).send("Compiled bytecode exceeds client network buffer"); + return; + } + + mqttClient.publish( + `cheriot-smartmeter/u/js/${req.params.meter}`, + vmresult.result, + { qos: 2, retain: false }, + (error) => + { + if (error) + { + console.error("MQTT Publish", error) + res.status(500).send(error); + } + else + { + res.status(200).send("OK"); + } + }); + }); + +expressApp.listen(PORT, () => + { + console.log(`server listening on ${PORT}`); + }); diff --git a/smartmeter/frontends/static.in/user-editor.mjs b/smartmeter/frontends/static.in/user-editor.mjs new file mode 100644 index 0000000..4798d1f --- /dev/null +++ b/smartmeter/frontends/static.in/user-editor.mjs @@ -0,0 +1,170 @@ +import {EditorView, basicSetup} from "codemirror" +import {javascript} from "@codemirror/lang-javascript" +import {history, indentWithTab} from "@codemirror/commands" +import {keymap, showPanel} from "@codemirror/view" + +import * as democonfig from "./democonfig.mjs"; + +var lastSubmittedEditorState; + +const examples = + [ ["Initial policy", "/static/js-examples/initial.txt" ] + , ["Hello LEDs", "/static/js-examples/hello-leds.txt" ] + , ["Attack (zero sensor data)", "/static/js-examples/attack-zero.txt" ] + , ["Crash", "/static/js-examples/crash.txt" ] + , ["Attack (pointer forging example)", "/static/js-examples/attack-cgp.txt" ] + ]; + +function topPanelCtor(view) +{ + const dom = document.createElement("div"); + + const exampleSelector = document.createElement("select"); + exampleSelector.required = false; + exampleSelector.selectedIndex = -1; + + { + const opt = document.createElement("option"); + opt.value = -1; + opt.text = "Load an example script"; + opt.hidden = true; + opt.disabled = true; + opt.selected = true; + exampleSelector.appendChild(opt); + } + + for (var i = 0; i < examples.length; i++) + { + const opt = document.createElement("option"); + opt.value = i; + opt.text = examples[i][0]; + exampleSelector.appendChild(opt); + } + + exampleSelector.addEventListener("change", async (event) => + { + const text = await fetch(examples[event.target.selectedOptions[0].value][1]); + exampleSelector.selectedIndex = 0; + view.dispatch( + { changes: + { insert: await text.text() + , from: 0 + , to: view.state.doc.length + } + }); + } + ); + + dom.append(exampleSelector); + + return { top: true + , dom + , update: (update) => + { + if (update.docChanged) + { + exampleSelector.selectedIndex = 0; + } + } + + }; +} + +function topPanelExt() +{ + return showPanel.of(topPanelCtor); +} + +function bottomPanelCtor(view) +{ + const dom = document.createElement("div"); + + const submitButton = document.createElement("button"); + submitButton.type = "submit"; + submitButton.innerHTML = "Compile and run!"; + dom.append(submitButton); + + const revertButton = document.createElement("button"); + revertButton.type = "button"; + revertButton.innerHTML = "Revert to last submitted version"; + revertButton.addEventListener("click", (event) => + { + view.setState(lastSubmittedEditorState); + } + ); + + dom.append(revertButton); + + const resultField = document.createElement("div"); + resultField.id = "theResult"; + resultField.innerHTML = ""; + dom.append(resultField); + + return { dom + , update: (update) => + { + if (update.docChanged) + { + resultField.innerHTML = "Local changes awaiting submission"; + } + } + }; +} + +function bottomPanelExt() +{ + return showPanel.of(bottomPanelCtor); +} + +const initialTextResponse = await fetch("/static/js-examples/initial.txt"); +const initialText = await initialTextResponse.text(); + +const theForm = document.getElementById("theForm"); +const meterIdentityInput = document.getElementById("meterIdentity"); + +if (democonfig.DEFAULT_METER_ID !== null) +{ + meterIdentityInput.value = democonfig.DEFAULT_METER_ID; +} + +let editor = new EditorView({ + extensions: [ basicSetup + , history() + , javascript() + , keymap.of([indentWithTab]) + , topPanelExt() + , bottomPanelExt() + ], + parent: theForm, + doc: initialText, +}) + +lastSubmittedEditorState = editor.state; + +theForm.addEventListener("submit", (event) => + { + event.preventDefault(); + + const resultField = document.getElementById("theResult"); + const meterIdentity = meterIdentityInput.value; + + const editorState = editor.state; + + fetch(`/postjs/${meterIdentity}`, + { method: "POST", + body: editorState.doc, + headers: { "Content-Type": "text/plain" } + }) + .then((response) => { + if (response.ok) + { + theResult.innerHTML = "OK!"; + lastSubmittedEditorState = editorState; + } + else + { + response.text().then((msg) => { theResult.innerHTML = msg; }) + }}) + .catch((err) => { theResult.innerHTML = error; }); + + }); diff --git a/smartmeter/frontends/static.in/user-editor.rollup-config.mjs b/smartmeter/frontends/static.in/user-editor.rollup-config.mjs new file mode 100644 index 0000000..4b013f2 --- /dev/null +++ b/smartmeter/frontends/static.in/user-editor.rollup-config.mjs @@ -0,0 +1,8 @@ +import pluginNodeResolve from "@rollup/plugin-node-resolve"; + +export default +{ input: "./static.in/user-editor.mjs" +, plugins: [ pluginNodeResolve() ] +, output: { file: "./static/user-editor.js", format: "es" } +, external: [ "./democonfig.mjs" ] +} diff --git a/smartmeter/frontends/static/.gitignore b/smartmeter/frontends/static/.gitignore new file mode 100644 index 0000000..34b7129 --- /dev/null +++ b/smartmeter/frontends/static/.gitignore @@ -0,0 +1 @@ +/user-editor.js diff --git a/smartmeter/frontends/static/cheriot.demo-config.mjs b/smartmeter/frontends/static/cheriot.demo-config.mjs new file mode 100644 index 0000000..bfe2d89 --- /dev/null +++ b/smartmeter/frontends/static/cheriot.demo-config.mjs @@ -0,0 +1,5 @@ +export const MQTT_BROKER_MQTTS_URL = "mqtts://cheriot.demo:8883"; +export const MQTT_BROKER_WSS_URL = "ws://cheriot.demo:8080/x"; +export const MQTT_BROKER_CERT = "./cheriot.demo.crt"; +export const SECURE_CERT = false; // cert is self-signed and key is public +export const DEFAULT_METER_ID = "SCIHouse"; diff --git a/smartmeter/frontends/static/cheriot.demo.crt b/smartmeter/frontends/static/cheriot.demo.crt new file mode 100644 index 0000000..b0558f7 --- /dev/null +++ b/smartmeter/frontends/static/cheriot.demo.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhDCCASmgAwIBAgIULGaRELkYWft0FpavC56TNbZsnKwwCgYIKoZIzj0EAwIw +FzEVMBMGA1UEAwwMY2hlcmlvdC5kZW1vMB4XDTI1MDQyMzE3MzIxMFoXDTM1MDQy +MTE3MzIxMFowFzEVMBMGA1UEAwwMY2hlcmlvdC5kZW1vMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAErQIlPQhP4GGZZ/+XLuh+LJEXy0/n1B1/aziHPCuVwfzZDJCK +jUQofS8PGGvp2iDbmt+pM3ESxAw3EbYSJ1KuEKNTMFEwHQYDVR0OBBYEFOVYQawJ +RTc9Jq2jVY1BWPPFdT+EMB8GA1UdIwQYMBaAFOVYQawJRTc9Jq2jVY1BWPPFdT+E +MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANzRqOFSb84s2l4s +06YUmfIkhM56t/i0uvzQ8fLTm+xTAiEA2wmwWBeen+3l17bGBzGAS0Ym0VQd0EXP +g22Uf0NDdB8= +-----END CERTIFICATE----- diff --git a/smartmeter/frontends/static/democonfig.mjs b/smartmeter/frontends/static/democonfig.mjs new file mode 120000 index 0000000..20bd6ca --- /dev/null +++ b/smartmeter/frontends/static/democonfig.mjs @@ -0,0 +1 @@ +mosquitto.org-config.mjs \ No newline at end of file diff --git a/smartmeter/frontends/static/grid.html b/smartmeter/frontends/static/grid.html new file mode 100644 index 0000000..6d9c9c1 --- /dev/null +++ b/smartmeter/frontends/static/grid.html @@ -0,0 +1,47 @@ + + + + Grid View + + + + + + + +

Grid Operator View

+ +
+ + + +
+ +
+ + 0 +
+ +

Consumption report

+
+ +

Send Grid Alert

+
+ For the next + + minutes, tell the house to + + . + +
+ + + diff --git a/smartmeter/frontends/static/grid.js b/smartmeter/frontends/static/grid.js new file mode 100644 index 0000000..9a593df --- /dev/null +++ b/smartmeter/frontends/static/grid.js @@ -0,0 +1,218 @@ +import * as democonfig from "./democonfig.mjs"; + +///// +// Snoop on the "user" timebase so that "now" evolves in parallel +///// + +var timebaseZero; +var timebaseRate; + +function timebaseRetime(sec) +{ + if (timebaseZero === null) + { + return sec; + } + + if (sec >= timebaseZero) + { + return timebaseZero + timebaseRate * (sec - timebaseZero); + } + + return sec; +} + +///// +// MQTT Client +///// + +const mqttClient = mqtt.connect( + democonfig.MQTT_BROKER_WSS_URL, + { clean: true + , connectTimeout: 4000 + }); + +mqttClient.on("message", (t, m) => + { + const mString = m.toString(); + console.log("Message", t.toString(), mString); + if (t.startsWith("cheriot-smartmeter/g/update/")) + { + const report = /^\s*(\d+)\s+(-?\d+)\s+.*$/.exec(mString); + updateConsumptionData(report); + } + else if (t.startsWith("cheriot-smartmeter/g/crash/")) + { + const report = /^\s*(\d+)\s*$/.exec(mString); + updateCrashData(report[1]); + } + else if (t.startsWith("cheriot-smartmeter/u/timebase")) + { + const newTimebase = /^\s*(\d+)\s+(\d+)\s*$/.exec(mString); + timebaseZero = parseInt(newTimebase[1]); + timebaseRate = parseInt(newTimebase[2]); + console.log(`New timebase: ${timebaseZero} ${timebaseRate}`); + } + }); +mqttClient.on("connect", () => + { + console.log("Connected"); + }); + +///// +// Crash count +///// + +const meterCrashForm = document.getElementById("meterCrashForm"); +const meterCrashCount = meterCrashForm.querySelector("#meterCrashes"); + +function updateCrashData(count) +{ + meterCrashCount.innerText = count; + meterCrashForm.animate([ { backgroundColor: "red" } + , { backgroundColor: "inherit" } + ], + { duration: 2000, iterations: 1 }); +} + +///// +// Consumption chart +///// + +let consumptionData = []; + +const consumptionChart = new Chart(document.getElementById("consumptionChart"), + { type: 'line' + , data: + { datasets: [ {label: 'Draw', data: consumptionData } ] } + , options: + { scales: + { x: + { title: + { display: true + , text: "Time" + } + , type: 'time' + , time: + { minUnit: 'second' } + } + } + } + }); + +function updateConsumptionData(report) +{ + const now = parseInt(report[1]) * 1000; // JS likes to work in mSec + + // Assume we haven't missed anything + consumptionData.push({ x: now, y: parseInt(report[2]) }) + + // Prune old data + consumptionData.sort((a,b) => a.x - b.x); + const firstKeep = consumptionData.findIndex( + (dataElement) => dataElement.x >= now - 7200000); + if (firstKeep > 0) + { + consumptionData.splice(0, firstKeep - 1); + } + + consumptionChart.update(); +} + +///// +// Grid alert form handling +///// + +const gridAlertForm = document.getElementById("gridAlertForm"); +const gridAlertDuration = gridAlertForm.querySelector("#gridAlertDuration"); +const gridAlertSelect = gridAlertForm.querySelector("#gridAlertSelect"); +const gridAlertSubmit = gridAlertForm.querySelector("#gridAlertSubmit"); + +gridAlertForm.addEventListener("submit", (event) => + { + event.preventDefault(); + + const now = timebaseRetime(Date.now() / 1000 | 0); + const duration = gridAlertDuration.value * 60; + const value = gridAlertSelect.value; + + console.log(`Publishing grid alert: ${lastMeterId} ${now} ${duration} ${value}`); + if (mqttClient) + { + mqttClient.publish( + `cheriot-smartmeter/g/request/${lastMeterId}`, + `${now} ${duration} ${value}`, + { qos: 1 }); + } + }); + + +///// +// Meter ID form handling +///// + +const meterIDForm = document.getElementById("meterIDForm"); +const meterIDValue = meterIDForm.querySelector("#meterIdentity"); +const meterIDSubmit = meterIDForm.querySelector("#meterIDSubmit"); + +function buttonEnables(needMeterID) +{ + meterIDSubmit.disabled = !needMeterID; + gridAlertSubmit.disabled = needMeterID; +} + +var lastMeterId; // for unsubscription +function onNewMeterID(newID) +{ + if (mqttClient && lastMeterId !== null) + { + mqttClient.unsubscribe(`cheriot-smartmeter/g/update/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to unsubscribe from updates?", err); } } + ); + mqttClient.unsubscribe(`cheriot-smartmeter/g/crash/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to unsubscribe from crashes?", err); } } + ); + mqttClient.unsubscribe(`cheriot-smartmeter/u/timebase/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to unsubscribe from timebase?", err); } } + ); + } + + lastMeterId = newID; + timebaseZero = null; + + consumptionData.splice(0, Infinity); + consumptionChart.update(); + + if (mqttClient) + { + mqttClient.subscribe(`cheriot-smartmeter/g/update/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to subscribe?", err); } } + ); + mqttClient.subscribe(`cheriot-smartmeter/g/crash/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to subscribe to crashes?", err); } } + ); + mqttClient.subscribe(`cheriot-smartmeter/u/timebase/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to subscribe to timebase?", err); } } + ); + + } + + buttonEnables(false); +} + +meterIDValue.addEventListener("input", (event) => + { + buttonEnables(true); + }); + +meterIDForm.addEventListener("submit", (event) => + { + event.preventDefault(); + onNewMeterID(meterIDValue.value); + }); + +if (democonfig.DEFAULT_METER_ID !== null) +{ + meterIDValue.value = democonfig.DEFAULT_METER_ID; + onNewMeterID(meterIDValue.value); +} diff --git a/smartmeter/frontends/static/index.html b/smartmeter/frontends/static/index.html new file mode 100644 index 0000000..78d806b --- /dev/null +++ b/smartmeter/frontends/static/index.html @@ -0,0 +1,10 @@ + + + SCI Smart Meter Demo + + + + + + + \ No newline at end of file diff --git a/smartmeter/frontends/static/js-examples/attack-cgp.txt b/smartmeter/frontends/static/js-examples/attack-cgp.txt new file mode 100644 index 0000000..a2e20d9 --- /dev/null +++ b/smartmeter/frontends/static/js-examples/attack-cgp.txt @@ -0,0 +1,24 @@ +// Import everything from the environment +import * as host from "./ffi.js" +import * as sm from "./smartmeter.js" + +const SENSOR_OFFSET_FINE = -120; +const SENSOR_OFFSET_COARSE = -80; + +function run() +{ + // The device exposes the provider's rate schedule as an array to our + // capability register machine. Have it land in register 1. + // We can think of this as an information leak of the address rather than a + // pointer proper, perhaps. + host.read_from_snapshot(host.DATA_PROVIDER_SCHEDULE_RATE_ARRAY, 1); + const rate_array_address = host.get_address(1); + + // So let's say we had a pointer forging gadget... + host.register_move(2, host.CGP); + host.set_address(2, rate_array_address); + host.print("Reading sensor data via CGP ", + host.load_int(2, SENSOR_OFFSET_FINE)); +} + +vmExport(1234, run); diff --git a/smartmeter/frontends/static/js-examples/attack-zero.txt b/smartmeter/frontends/static/js-examples/attack-zero.txt new file mode 100644 index 0000000..eb0d222 --- /dev/null +++ b/smartmeter/frontends/static/js-examples/attack-zero.txt @@ -0,0 +1,33 @@ +// Import everything from the environment +import * as host from "./ffi.js" +import * as sm from "./smartmeter.js" + +const SENSOR_OFFSET_FINE = -120; +const SENSOR_OFFSET_COARSE = -80; + +function run() +{ + // The device exposes the provider's rate schedule as an array to our + // capability register machine. Have it land in register 1. + host.read_from_snapshot(host.DATA_PROVIDER_SCHEDULE_RATE_ARRAY, 1); + host.print("Rate array address ", host.get_address(1)); + + // In fact, the rate array is right next to the sensor data in memory. We can + // verify that by asking the FFI for the sensor value (which will just have + // been snapshotted for us) as well as reading it from memory directly. + var ffi_sensor_value = host.read_from_snapshot(host.DATA_SENSOR_SAMPLE, 0); + var mem_sensor_value = host.load_int(1, SENSOR_OFFSET_FINE); + host.print("Sensor data: ", ffi_sensor_value, " ", mem_sensor_value); + + // We can go further and scribble on sensor reports! Choosing + // SENSOR_OFFSET_COARSE leaves the grid operator informed of the fine samples, + // but zeros out the provider's view. Choose SENSOR_OFFSET_FINE to zero out + // both reports (because a coarsened constant zero is still zero). + for (let i = 0; i < 4; i++) + { + // Each store writes two words; register -1 always holds null (zero) + host.store(-1, 1, SENSOR_OFFSET_COARSE + i * 8); + } +} + +vmExport(1234, run); diff --git a/smartmeter/frontends/static/js-examples/crash.txt b/smartmeter/frontends/static/js-examples/crash.txt new file mode 100644 index 0000000..514b35d --- /dev/null +++ b/smartmeter/frontends/static/js-examples/crash.txt @@ -0,0 +1,9 @@ +import * as host from "./ffi.js" + +function run() +{ + // Attempt to write to null. This crashes. + host.store(-1, 0, 0); +} + +vmExport(1234, run); diff --git a/smartmeter/frontends/static/js-examples/hello-leds.txt b/smartmeter/frontends/static/js-examples/hello-leds.txt new file mode 100644 index 0000000..2385f95 --- /dev/null +++ b/smartmeter/frontends/static/js-examples/hello-leds.txt @@ -0,0 +1,31 @@ +// Import everything from the environment +import * as host from "./ffi.js"; + +var run_once = false; +var tick = 0; +var direction = false; + +function run() +{ + if (!run_once) + { + host.print("Hello world!") + run_once = true + for (var i = 0; i < 8; i++) + { + host.led_set(i, false) + } + } + + var led = tick % 8 + if (led === 0) + { + direction = !direction + } + host.print("Setting LED ", led, " to ", direction) + host.led_set(led, direction) + tick++ +} + + +vmExport(1234, run); diff --git a/smartmeter/frontends/static/js-examples/initial.txt b/smartmeter/frontends/static/js-examples/initial.txt new file mode 120000 index 0000000..4ce928f --- /dev/null +++ b/smartmeter/frontends/static/js-examples/initial.txt @@ -0,0 +1 @@ +../../../cheriot/js/sample_policy.js \ No newline at end of file diff --git a/smartmeter/frontends/static/mosquitto.org-config.mjs b/smartmeter/frontends/static/mosquitto.org-config.mjs new file mode 100644 index 0000000..c47b3d2 --- /dev/null +++ b/smartmeter/frontends/static/mosquitto.org-config.mjs @@ -0,0 +1,5 @@ +export const MQTT_BROKER_MQTTS_URL = "mqtts://test.mosquitto.org:8883"; +export const MQTT_BROKER_WSS_URL = "wss://test.mosquitto.org:8081/x"; +export const MQTT_BROKER_CERT = "./mosquitto.org.crt"; +export const SECURE_CERT = true; +export const DEFAULT_METER_ID = null; diff --git a/smartmeter/frontends/static/provider.html b/smartmeter/frontends/static/provider.html new file mode 100644 index 0000000..a2d2f31 --- /dev/null +++ b/smartmeter/frontends/static/provider.html @@ -0,0 +1,47 @@ + + + + Provider View + + + + + + + +

Provider Operator View

+ +
+ + + +
+ +

Consumption report

+
+ +

Set Power Rates

+
+ +
+ + + + + +
+ + + diff --git a/smartmeter/frontends/static/provider.js b/smartmeter/frontends/static/provider.js new file mode 100644 index 0000000..d554b79 --- /dev/null +++ b/smartmeter/frontends/static/provider.js @@ -0,0 +1,295 @@ +import * as democonfig from "./democonfig.mjs"; + +///// +// Snoop on the "user" timebase so that "now" evolves in parallel +///// + +var timebaseZero; +var timebaseRate; + +function timebaseRetime(sec) +{ + if (timebaseZero === null) + { + return sec; + } + + if (sec >= timebaseZero) + { + return timebaseZero + timebaseRate * (sec - timebaseZero); + } + + return sec; +} + +var lastMidnight = null; +var lastTimestamp = null; + +function updateTimestamps() +{ + const now = Date.now(); + lastTimestamp = timebaseRetime(now); + lastMidnight = new Date(lastTimestamp); + lastMidnight.setHours(0, 0, 0); + + // console.log("updateTimestamps", now, lastTimestamp, lastMidnight); +} + +function displayHour() +{ + if (lastTimestamp === null) + { + // console.log("displayHour null"); + return -1; + } + + const ret = (lastTimestamp - lastMidnight.getTime()) / 1000 / 3600; + // console.log("displayHour", ret); + + return ret; +} + +///// +// MQTT Client +///// + +const mqttClient = mqtt.connect( + democonfig.MQTT_BROKER_WSS_URL, + { clean: true + , connectTimeout: 4000 + }); + +mqttClient.on("message", (t, m) => + { + const mString = m.toString(); + console.log("Message", t.toString(), mString); + if (t.startsWith("cheriot-smartmeter/p/update")) + { + const report = /^\s*(\d+)\s+(-?\d+)\s+.*$/.exec(mString); + updateConsumptionData(report); + } + else if (t.startsWith("cheriot-smartmeter/u/timebase")) + { + const newTimebase = /^\s*(\d+)\s+(\d+)\s*$/.exec(mString); + timebaseZero = parseInt(newTimebase[1]); + timebaseRate = parseInt(newTimebase[2]); + console.log(`New timebase: ${timebaseZero} ${timebaseRate}`); + } + + }); +mqttClient.on("connect", () => + { + console.log("Connected"); + }); + +///// +// Consumption chart +///// + +let consumptionData = []; + +const consumptionChart = new Chart(document.getElementById("consumptionChart"), + { type: 'line' + , data: + { datasets: [ {label: 'Draw', data: consumptionData } ] } + , options: + { scales: + { x: + { title: + { display: true + , text: "Time" + } + , type: 'time' + , time: + { minUnit: 'second' } + } + } + } + }); + +function updateConsumptionData(report) +{ + const now = parseInt(report[1]) * 1000; // JS likes to work in mSec + + // Assume we haven't missed anything + consumptionData.push({ x: now, y: parseInt(report[2]) }) + + // Prune old data + consumptionData.sort((a,b) => a.x - b.x); + const firstKeep = consumptionData.findIndex( + (dataElement) => dataElement.x >= now - 7200000); + if (firstKeep > 0) + { + consumptionData.splice(0, firstKeep - 1); + } + + consumptionChart.update(); +} + +///// +// Rate data and chart +///// + +const rateTables = + { flat: Array.from({length: 24}, (v, i) => 760) + , winterwk: + [760,760,760,760,760,760,760,760,1580,1580,1580,1220,1220,1220,1220,1580,1580,1580,760,760,760,760,760,760] + , summerwk: + [760,760,760,760,760,760,760,760,1220,1220,1220,1580,1580,1580,1580,1220,1220,1220,760,760,760,760,760,760] + , ulowe: + [280,280,280,280,280,280,280,760,760,760,760,760,760,760,760,760,760,760,760,760,760,760,760,280] + } + +const rateDisplayData = new Array(48); + +const rateChart = new Chart(document.getElementById("powerRateChart"), + { type: 'line' + , data: + { labels: [...rateDisplayData.keys()] + , datasets: [{ label: 'Rate', data: rateDisplayData, borderWidth: 1 }] + } + , options: + { scales: + { x: + { title: + { display: true + , text: "Hours since today's midnight" + } + , beginAtZero: true + , min: 0 + , max: 47 + } + , y: + { beginAtZero: true + , title: + { display: true + , text: "p/kWh" + } + , ticks: + { callback: (value, index, ticks) => value.toFixed(1) } + } + } + , plugins: + { annotation: + { common: { drawTime: "afterDraw" } + , annotations: + { line1: + { type: 'line' + , borderColor: 'rgb(255, 99, 132)' + , borderWidth: 2 + , scaleID: 'x' + , value: (ctx, obj) => displayHour() + , endValue: (ctx, obj) => displayHour() + , display: (ctx, obj) => lastMidnight !== null + } + } + } + } + } + }); + +const rateData = new Array(48); + +const rateForm = document.getElementById("providerRateForm"); +const rateTodaySelect = rateForm.querySelector("#providerRateToday"); +const rateTomorrowSelect = rateForm.querySelector("#providerRateTomorrow"); +const rateSubmit = rateForm.querySelector("#providerRateSubmit"); + +function setRateData(anim) +{ + rateData.splice(0, 24, ...rateTables[rateTodaySelect.value]); + rateData.splice(24, 24, ...rateTables[rateTomorrowSelect.value]); + rateDisplayData.splice(0, 48, ...rateData.map((v) => v/100)); + rateChart.update(anim); +} + +rateTodaySelect.addEventListener("input", (event) => + { + setRateData(); + }); +rateTomorrowSelect.addEventListener("input", (event) => + { + setRateData(); + }); + +rateForm.addEventListener("submit", (event) => + { + event.preventDefault(); + + const rateMessage = `${lastMidnight.getTime() / 1000 | 0} ${rateData.map((v) => v.toString()).join(" ")}`; + console.log("Rate message", rateMessage); + if (mqttClient) + { + mqttClient.publish( + `cheriot-smartmeter/p/schedule/${lastMeterId}`, rateMessage, { qos: 1 }); + } + }); + +///// +// Meter ID form handling +///// + +const meterIDForm = document.getElementById("meterIDForm"); +const meterIDValue = meterIDForm.querySelector("#meterIdentity"); +const meterIDSubmit = meterIDForm.querySelector("#meterIDSubmit"); + +function buttonEnables(needMeterID) +{ + meterIDSubmit.disabled = !needMeterID; + rateSubmit.disabled = needMeterID; +} + +var lastMeterId; // for unsubscription +function onNewMeterID(newID) +{ + if (mqttClient && lastMeterId !== null) + { + mqttClient.unsubscribe(`cheriot-smartmeter/p/update/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to unsubscribe from updates?", err); } } + ); + mqttClient.unsubscribe(`cheriot-smartmeter/u/timebase/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to unsubscribe from timebase?", err); } } + ); + } + + lastMeterId = newID; + + if (mqttClient) + { + mqttClient.subscribe(`cheriot-smartmeter/p/update/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to subscribe?", err); } } + ); + mqttClient.subscribe(`cheriot-smartmeter/u/timebase/${lastMeterId}`, + (err) => { if(err) { console.log("Failed to subscribe to timebase?", err); } } + ); + } + + buttonEnables(false); +} + +meterIDValue.addEventListener("input", (event) => + { + buttonEnables(true); + }); + +meterIDForm.addEventListener("submit", (event) => + { + event.preventDefault(); + onNewMeterID(meterIDValue.value); + }); + +if (democonfig.DEFAULT_METER_ID !== null) +{ + meterIDValue.value = democonfig.DEFAULT_METER_ID; + onNewMeterID(meterIDValue.value); +} + +function allUpdates() +{ + // Initialize with default settings + updateTimestamps(); + setRateData('none'); +} + +allUpdates(); +setInterval(allUpdates, 5000); diff --git a/smartmeter/frontends/static/user-editor.html b/smartmeter/frontends/static/user-editor.html new file mode 100644 index 0000000..05bc34b --- /dev/null +++ b/smartmeter/frontends/static/user-editor.html @@ -0,0 +1,16 @@ + + + + User Policy Editor View + + +

User Policy Editor

+
+ + + +
+ + diff --git a/smartmeter/house/README.md b/smartmeter/house/README.md new file mode 100644 index 0000000..9e4a565 --- /dev/null +++ b/smartmeter/house/README.md @@ -0,0 +1,13 @@ +# CHERIoT smart meter demo -- house code + +This directory contains micropython code to run on the esp32 board supplied +with the KS5009 "Smart Home" used for the demo. + +## Installation + +Assuming the esp32 is already flashed with MicroPython you can use `rshell` +(available via `pip`) to install the python files on the board, for example: + +``` +rshell -p /dev/tty.wchusbserial110 rsync python/ /pyboard/ +``` diff --git a/smartmeter/house/python/boot.py b/smartmeter/house/python/boot.py new file mode 100644 index 0000000..1870ef0 --- /dev/null +++ b/smartmeter/house/python/boot.py @@ -0,0 +1,3 @@ +# This is script that run when device boot up or wake from sleep. + + diff --git a/smartmeter/house/python/lib/i2c_lcd.py b/smartmeter/house/python/lib/i2c_lcd.py new file mode 100644 index 0000000..3b905c2 --- /dev/null +++ b/smartmeter/house/python/lib/i2c_lcd.py @@ -0,0 +1,95 @@ +# This file was distributed with the Keystudio KS5009 tutorial without copyright +# information. https://github.com/keyestudio/KS5009-Keyestudio-Smart-Home-Kit-for-ESP32 +# It appears to be derived from https://github.com/dhylands/python_lcd/ +# which contains the following: +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Dave Hylands +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Implements a HD44780 character LCD connected via PCF8574 on I2C. + This was tested with: https://www.wemos.cc/product/d1-mini.html""" +from lcd_api import LcdApi +from machine import I2C +from time import sleep_ms +# The PCF8574 has a jumper selectable address: 0x20 - 0x27 +#DEFAULT_I2C_ADDR = 0x20 +# Defines shifts or masks for the various LCD line attached to the PCF8574 +MASK_RS = 0x01 +MASK_RW = 0x02 +MASK_E = 0x04 +SHIFT_BACKLIGHT = 3 +SHIFT_DATA = 4 +class I2cLcd(LcdApi): + """Implements a HD44780 character LCD connected via PCF8574 on I2C.""" + def __init__(self, i2c, i2c_addr, num_lines, num_columns): + self.i2c = i2c + self.i2c_addr = i2c_addr + self.i2c.writeto(self.i2c_addr, bytearray([0])) + sleep_ms(20) # Allow LCD time to powerup + # Send reset 3 times + self.hal_write_init_nibble(self.LCD_FUNCTION_RESET) + sleep_ms(5) # need to delay at least 4.1 msec + self.hal_write_init_nibble(self.LCD_FUNCTION_RESET) + sleep_ms(1) + self.hal_write_init_nibble(self.LCD_FUNCTION_RESET) + sleep_ms(1) + # Put LCD into 4 bit mode + self.hal_write_init_nibble(self.LCD_FUNCTION) + sleep_ms(1) + LcdApi.__init__(self, num_lines, num_columns) + cmd = self.LCD_FUNCTION + if num_lines > 1: + cmd |= self.LCD_FUNCTION_2LINES + self.hal_write_command(cmd) + def hal_write_init_nibble(self, nibble): + """Writes an initialization nibble to the LCD. + This particular function is only used during initialization. + """ + byte = ((nibble >> 4) & 0x0f) << SHIFT_DATA + self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E])) + self.i2c.writeto(self.i2c_addr, bytearray([byte])) + def hal_backlight_on(self): + """Allows the hal layer to turn the backlight on.""" + self.i2c.writeto(self.i2c_addr, bytearray([1 << SHIFT_BACKLIGHT])) + def hal_backlight_off(self): + """Allows the hal layer to turn the backlight off.""" + self.i2c.writeto(self.i2c_addr, bytearray([0])) + def hal_write_command(self, cmd): + """Writes a command to the LCD. + Data is latched on the falling edge of E. + """ + byte = ((self.backlight << SHIFT_BACKLIGHT) | (((cmd >> 4) & 0x0f) << SHIFT_DATA)) + self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E])) + self.i2c.writeto(self.i2c_addr, bytearray([byte])) + byte = ((self.backlight << SHIFT_BACKLIGHT) | ((cmd & 0x0f) << SHIFT_DATA)) + self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E])) + self.i2c.writeto(self.i2c_addr, bytearray([byte])) + if cmd <= 3: + # The home and clear commands require a worst case delay of 4.1 msec + sleep_ms(5) + def hal_write_data(self, data): + """Write data to the LCD.""" + byte = (MASK_RS | (self.backlight << SHIFT_BACKLIGHT) | (((data >> 4) & 0x0f) << SHIFT_DATA)) + self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E])) + self.i2c.writeto(self.i2c_addr, bytearray([byte])) + byte = (MASK_RS | (self.backlight << SHIFT_BACKLIGHT) | ((data & 0x0f) << SHIFT_DATA)) + self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E])) + self.i2c.writeto(self.i2c_addr, bytearray([byte])) diff --git a/smartmeter/house/python/lib/lcd_api.py b/smartmeter/house/python/lib/lcd_api.py new file mode 100644 index 0000000..c08de92 --- /dev/null +++ b/smartmeter/house/python/lib/lcd_api.py @@ -0,0 +1,198 @@ +# This file was distributed with the Keystudio KS5009 tutorial without copyright +# information. https://github.com/keyestudio/KS5009-Keyestudio-Smart-Home-Kit-for-ESP32 +# It appears to be derived from https://github.com/dhylands/python_lcd/ +# which contains the following: +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Dave Hylands +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Provides an API for talking to HD44780 compatible character LCDs.""" +import time +class LcdApi: + """Implements the API for talking with HD44780 compatible character LCDs. + This class only knows what commands to send to the LCD, and not how to get + them to the LCD. + It is expected that a derived class will implement the hal_xxx functions. + """ + # The following constant names were lifted from the avrlib lcd.h + # header file, however, I changed the definitions from bit numbers + # to bit masks. + # + # HD44780 LCD controller command set + LCD_CLR = 0x01 # DB0: clear display + LCD_HOME = 0x02 # DB1: return to home position + LCD_ENTRY_MODE = 0x04 # DB2: set entry mode + LCD_ENTRY_INC = 0x02 # --DB1: increment + LCD_ENTRY_SHIFT = 0x01 # --DB0: shift + LCD_ON_CTRL = 0x08 # DB3: turn lcd/cursor on + LCD_ON_DISPLAY = 0x04 # --DB2: turn display on + LCD_ON_CURSOR = 0x02 # --DB1: turn cursor on + LCD_ON_BLINK = 0x01 # --DB0: blinking cursor + LCD_MOVE = 0x10 # DB4: move cursor/display + LCD_MOVE_DISP = 0x08 # --DB3: move display (0-> move cursor) + LCD_MOVE_RIGHT = 0x04 # --DB2: move right (0-> left) + LCD_FUNCTION = 0x20 # DB5: function set + LCD_FUNCTION_8BIT = 0x10 # --DB4: set 8BIT mode (0->4BIT mode) + LCD_FUNCTION_2LINES = 0x08 # --DB3: two lines (0->one line) + LCD_FUNCTION_10DOTS = 0x04 # --DB2: 5x10 font (0->5x7 font) + LCD_FUNCTION_RESET = 0x30 # See "Initializing by Instruction" section + LCD_CGRAM = 0x40 # DB6: set CG RAM address + LCD_DDRAM = 0x80 # DB7: set DD RAM address + LCD_RS_CMD = 0 + LCD_RS_DATA = 1 + LCD_RW_WRITE = 0 + LCD_RW_READ = 1 + def __init__(self, num_lines, num_columns): + self.num_lines = num_lines + if self.num_lines > 4: + self.num_lines = 4 + self.num_columns = num_columns + if self.num_columns > 40: + self.num_columns = 40 + self.cursor_x = 0 + self.cursor_y = 0 + self.implied_newline = False + self.backlight = True + self.display_off() + self.backlight_on() + self.clear() + self.hal_write_command(self.LCD_ENTRY_MODE | self.LCD_ENTRY_INC) + self.hide_cursor() + self.display_on() + def clear(self): + """Clears the LCD display and moves the cursor to the top left + corner. + """ + self.hal_write_command(self.LCD_CLR) + self.hal_write_command(self.LCD_HOME) + self.cursor_x = 0 + self.cursor_y = 0 + def show_cursor(self): + """Causes the cursor to be made visible.""" + self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | + self.LCD_ON_CURSOR) + def hide_cursor(self): + """Causes the cursor to be hidden.""" + self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY) + def blink_cursor_on(self): + """Turns on the cursor, and makes it blink.""" + self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | + self.LCD_ON_CURSOR | self.LCD_ON_BLINK) + def blink_cursor_off(self): + """Turns on the cursor, and makes it no blink (i.e. be solid).""" + self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | + self.LCD_ON_CURSOR) + def display_on(self): + """Turns on (i.e. unblanks) the LCD.""" + self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY) + def display_off(self): + """Turns off (i.e. blanks) the LCD.""" + self.hal_write_command(self.LCD_ON_CTRL) + def backlight_on(self): + """Turns the backlight on. + This isn't really an LCD command, but some modules have backlight + controls, so this allows the hal to pass through the command. + """ + self.backlight = True + self.hal_backlight_on() + def backlight_off(self): + """Turns the backlight off. + This isn't really an LCD command, but some modules have backlight + controls, so this allows the hal to pass through the command. + """ + self.backlight = False + self.hal_backlight_off() + def move_to(self, cursor_x, cursor_y): + """Moves the cursor position to the indicated position. The cursor + position is zero based (i.e. cursor_x == 0 indicates first column). + """ + self.cursor_x = cursor_x + self.cursor_y = cursor_y + addr = cursor_x & 0x3f + if cursor_y & 1: + addr += 0x40 # Lines 1 & 3 add 0x40 + if cursor_y & 2: # Lines 2 & 3 add number of columns + addr += self.num_columns + self.hal_write_command(self.LCD_DDRAM | addr) + def putchar(self, char): + """Writes the indicated character to the LCD at the current cursor + position, and advances the cursor by one position. + """ + if char == '\n': + if self.implied_newline: + # self.implied_newline means we advanced due to a wraparound, + # so if we get a newline right after that we ignore it. + pass + else: + self.cursor_x = self.num_columns + else: + self.hal_write_data(ord(char)) + self.cursor_x += 1 + if self.cursor_x >= self.num_columns: + self.cursor_x = 0 + self.cursor_y += 1 + self.implied_newline = (char != '\n') + if self.cursor_y >= self.num_lines: + self.cursor_y = 0 + self.move_to(self.cursor_x, self.cursor_y) + def putstr(self, string): + """Write the indicated string to the LCD at the current cursor + position and advances the cursor position appropriately. + """ + for char in string: + self.putchar(char) + def custom_char(self, location, charmap): + """Write a character to one of the 8 CGRAM locations, available + as chr(0) through chr(7). + """ + location &= 0x7 + self.hal_write_command(self.LCD_CGRAM | (location << 3)) + self.hal_sleep_us(40) + for i in range(8): + self.hal_write_data(charmap[i]) + self.hal_sleep_us(40) + self.move_to(self.cursor_x, self.cursor_y) + + def hal_backlight_on(self): + """Allows the hal layer to turn the backlight on. + If desired, a derived HAL class will implement this function. + """ + pass + def hal_backlight_off(self): + """Allows the hal layer to turn the backlight off. + If desired, a derived HAL class will implement this function. + """ + pass + def hal_write_command(self, cmd): + """Write a command to the LCD. + It is expected that a derived HAL class will implement this + function. + """ + raise NotImplementedError + def hal_write_data(self, data): + """Write data to the LCD. + It is expected that a derived HAL class will implement this + function. + """ + raise NotImplementedError + def hal_sleep_us(self, usecs): + """Sleep for some time (given in microseconds).""" + time.sleep_us(usecs) diff --git a/smartmeter/house/python/main.py b/smartmeter/house/python/main.py new file mode 100644 index 0000000..14ff8ae --- /dev/null +++ b/smartmeter/house/python/main.py @@ -0,0 +1,275 @@ +from time import sleep_ms, ticks_ms, ticks_add, ticks_diff +from machine import I2C, Pin, UART, PWM +from i2c_lcd import I2cLcd +import neopixel + +sonata_uart = UART(1, 9600, rx=13, tx=12) + +DEFAULT_I2C_ADDR = 0x27 +i2c = I2C(scl=Pin(22), sda=Pin(21), freq=400000) +lcd = I2cLcd(i2c, DEFAULT_I2C_ADDR, 2, 16) +lcd.clear() +''' +For given string consisting of 8 lines of 5 characters +return an 8 byte array where each byte has a bits set +For convenience when creating custom glyphs. +''' +def str_to_glyph_bytes(s): + def line_to_byte(l): + return sum([1 << n for n in range(5) if l[4-n]=='X']) + lines = s.strip().splitlines() + return bytearray([line_to_byte(l) for l in lines]) + +next_custom_char=0 +def make_custom_char(s): + global next_custom_char + if next_custom_char > 7: + raise Exception('Max 8 custom characters') + + char_bytes = str_to_glyph_bytes(s) + lcd.custom_char(next_custom_char, char_bytes) + custom_char = chr(next_custom_char) + next_custom_char += 1 + return custom_char + +cherry_left=make_custom_char(''' +....X +...X. +..X.. +.XXX. +XXXXX +XXXXX +XXXXX +.XXX. +''') +cherry_right=make_custom_char(''' +X.... +.X... +..X.. +.XXX. +XXXXX +XXXXX +XXXXX +.XXX. +''') + +house_char_str=''' +..X.. +.XXX. +XXXXX +.XXX. +.X.X. +.XXX. +.X.X. +.X.X. +''' + +# This char looks like a electricity pylon +grid_char=chr(0xb7) +# And this one a tiny bit like a turbine +turbine_char=chr(0xb2) + +marquee_text=f'SCI CHERIoT {cherry_left}{cherry_right} Smart Meter Demo {cherry_left}{cherry_right} ' +marquee_offset=0 + +def make_battery(n): + lines = ''' +.XXX. +XX.XX +''' + (5-n) * 'X...X\n' + (n+1) * 'XXXXX\n' + return make_custom_char(lines) +battery_chars = [make_battery(n) for n in range(5)] +battery_chars.append(make_custom_char('.XXX.\n' + 7 * 'XXXXX\n')) + +class BaseAppliance: + """ + Synchronise state with house e.g. write led + """ + def sync(self): + pass + +class OnOffAppliance: + def __init__(self, load): + self.load_when_on = load + def do_action(self, s): + self.is_on = s == b'on' + self.sync() + def get_load(self): + return self.load_when_on if self.is_on else 0 + def toggle(self): + print('toggle') + self.is_on = not self.is_on + self.sync() + +class YellowLed(OnOffAppliance): + def __init__(self): + OnOffAppliance.__init__(self, 1) + self.pin = Pin(17, Pin.OUT) + self.is_on = False + + def sync(self): + self.pin.value(1 if self.is_on else 0) + +class RGBLed(BaseAppliance): + RED = [x * 50 for x in (1,0,0)] + OFF = [0,0,0] + + def __init__(self): + OnOffAppliance.__init__(self, 4) + rgb_pin = Pin(16, Pin.OUT) + self.rgb_np = neopixel.NeoPixel(rgb_pin, 4) + self.load = 0 + def get_load(self): + return self.load + def sync(self): + for n in range(4): + c = RGBLed.RED if n < self.load else RGBLed.OFF + self.rgb_np[n]=c + self.rgb_np.write() + def do_action(self, s): + try: + self.load=int(s) + except: + self.load+=1 + if self.load < 0 or self.load > 4: + self.load=0 + self.sync() + +class Battery(BaseAppliance): + MAX_CHARGE=50 + MAX_RATE_OF_CHARGE=3 + def __init__(self): + self.state_of_charge=0 + self.net_target=0 + self.last_rate=0 + def get_load(self, other_load): + # at what rate do we aim to charge /discharge the battery + # we limit to between -MAX_RATE_OF_CHARGE, MAX_RATE_OF_CHARGE + target_rate = max(-self.MAX_RATE_OF_CHARGE, min(self.MAX_RATE_OF_CHARGE, self.net_target - other_load)) + discharging = self.state_of_charge > 0 and target_rate < 0 + charging = self.state_of_charge < self.MAX_CHARGE and target_rate > 0 + if charging or discharging: + next_soc = self.state_of_charge + target_rate + self.state_of_charge = max(0, min(self.MAX_CHARGE, next_soc)) + self.last_rate=target_rate + else: + self.last_rate=0 + return self.last_rate + def sync(self): + pass + def get_glyph(self): + return battery_chars[int(self.state_of_charge / 10)] + def do_action(self, s): + try: + self.net_target=int(s) + except: + print(f"Battery action failed: {s}") + +class Turbine(BaseAppliance): + def __init__(self): + self.wind=0 + self.pin=PWM(Pin(18,Pin.OUT),freq=1000,duty=0) + self.pin.duty(0) + self.pin2=PWM(Pin(19,Pin.OUT),freq=1000,duty=2) + self.pin2.duty(0) + def get_load(self): + return -self.wind + def sync(self): + duty = 0 + if self.wind > 0: + # 200 to 450 seems to be a sensible range + duty = 200 + 25 * self.wind + self.pin2.duty(duty) + def bump(self): + self.wind = self.wind + 1 + if self.wind > 5: + self.wind = 0 + self.sync() + def do_action(self, s): + pass + def deinit(self): + print('deinit') + self.pin.duty(0) + self.pin2.duty(0) + self.pin.deinit() + self.pin2.deinit() + + +yellow_led = YellowLed() +rgb = RGBLed() +turbine = Turbine() +battery = Battery() +appliances = { + b'led': yellow_led, + b'battery': battery, + b'turbine': turbine, + b'heatpump':rgb, +} +loads=[yellow_led, rgb] + +REPORT_INTERVAL=1000 +start_ticks = ticks_ms() +next_report = ticks_add(start_ticks, REPORT_INTERVAL) + +left_button=Pin(25, Pin.IN, Pin.PULL_UP) +right_button=Pin(26, Pin.IN, Pin.PULL_UP) +left_button_last_value=True +right_button_last_value=True + +# Let things settle for a minute. +sleep_ms(1000) + +try: + while True: + # The following combined with the sleep at the bottom of the loop + # is sufficient to debounce the buttons. We trigger on the falling + # edge (button first pressed). + left_button_value = left_button.value() + if left_button_last_value and not left_button_value: + rgb.do_action('') + left_button_last_value = left_button_value + right_button_value = right_button.value() + if right_button_last_value and not right_button_value: + turbine.bump() + right_button_last_value = right_button_value + + if ticks_diff(ticks_ms(), next_report) > 0: + house_load=0 + for a in loads: + house_load+=a.get_load() + a.sync() + # (negative) turbine load + turbine_load = turbine.get_load() + total_load = house_load + turbine_load + # The battery computes its load based on + # net_target and other house load + battery_load=battery.get_load(total_load) + total_load+=battery_load + battery.sync() + lcd.move_to(0,0) + lcd.putstr(f'H{house_load:+2} {turbine_char}{turbine_load:+2} {battery.get_glyph()}{battery_load:+2} {grid_char}{total_load:+2}') + lcd.move_to(0,1) + lcd.putstr(marquee_text[marquee_offset:marquee_offset+16]) + if marquee_offset + 16 > len(marquee_text): + lcd.putstr(marquee_text[0:marquee_offset+16 - len(marquee_text)]) + marquee_offset += 1 + if marquee_offset >= len(marquee_text): + marquee_offset = 0 + report=f'powerSample {total_load}\n' + print(report) + sonata_uart.write(report) + next_report = ticks_add(next_report, REPORT_INTERVAL) + command = sonata_uart.readline() + if command is not None: + print(command) + words = command.split() + if len(words) == 2: + [target, action] = command.split() + app = appliances.get(target, None) + if app is not None: + app.do_action(action) + else: + sleep_ms(100) +except Exception as e: + turbine.deinit() + raise e diff --git a/smartmeter/server_config/README.md b/smartmeter/server_config/README.md new file mode 100644 index 0000000..edeb363 --- /dev/null +++ b/smartmeter/server_config/README.md @@ -0,0 +1,92 @@ +# Server configuration notes for RPI500 used with smart meter demo + +## Networking + +RPI config tool ethernet manual config IP4 10.0.0.10 netmask 255.0.0.0 +Tick use only for traffic on its network. + +## Install DHCP/ DNS server dnsmasq + +dnsmasq +/etc/dnsmasq.conf +interface=eth0 +dhcp-range=10.0.0.11,10.254.254.254,12h +addn-hosts=/etc/demo.hosts + +/etc/hosts +10.0.0.10 cheriot.demo + +# Pretend to CHERIoT that RPi is pool.ntp.org for sntp +/etc/demo.hosts +10.0.0.10 pool.ntp.org + +## Configure ntpd +sudo apt install ntpsec + +/etc/ntpsec/ntp.conf +# tell NTP to work in offline mode after 10s without peers +tos orphan 15 orphanwait 10 + +# my router was advertising an ntp server but this was overriding ntp.conf +# and preventing synchronisation for some reason. Stop this. +/etc/default/ntpsec +IGNORE_DHCP=yes + +# mosquitto from debian might be OK but I added latest from mosquitto.org repo +cd /etc/apt/souces.list.d/ +sudo wget http://repo.mosquitto.org/debian/mosuitto-bookworm.list +cd /etc/apt/keyrings +sudo wget http://repo.mosqutto.org/debian/mosquitto-repo.gpg +sudo apt update +sudo apt install mosquitto + +/etc/mosquitto/conf.d/demo.conf +# unencrypted listener for local websockets +listener 8080 10.0.0.10 +protocol websockets +socket_domain ipv4 +log_type all +allow_anonymous true +connection_messages true + +# mqtt listener with self-signed demo cert and key +listener 8883 10.0.0.10 +protocol mqtt +tls_keyform pem +keyfile /var/lib/mosquitto/key.pem +certfile /var/lib/mosquitto/cert.pem +log_type all +allow_anonymous true +connection_messages true + +cp cheriot.demo.crt /var/lib/mosquitto/cert.pem +cp cheriot.demo.key /var/lib/mosquitto/key.pem + +# Get recent node from nodesource +curl -fsSL https://deb.nodesource.com/setup_23.x -o nodesource_setup.sh +sudo -E bash nodesource_setup.sh +sudo apt-get install -y nodejs + +# Run up frontend server as per ../frontends/README.md +npm install +./node_modules/.bin/rollup --config ./static.in/user-editor.rollup-config.mjs +node ./server.js + +# On MacOS +brew install dnsmasq mosquitto ntp +sudo /opt/homebrew/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C dnsmasq.conf +/opt/homebrew/opt/mosquitto/sbin/mosquitto -v -c mosquitto.conf +sudo /opt/homebrew/sbin/ntpd -d -c ntp.conf +picocom /dev/tty.usbserial-LN28282 -b 921600 --imap=lfcrlf +# /etc/hosts +10.0.0.10 cheriot.demo + +# Generating ssl cert. and bear SSL trust anchor header +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 3650 -nodes -keyout cheriot.demo.key -out cheriot.demo.crt -subj "/CN=cheriot.demo" +brssl ta cheriot.demo.crt > cheriot/cheriot.demo.h + +# Buidling smartmeter/cheriot +xmake config --sdk=~/llvm-project/Build/install --broker-host=cheriot.demo --broker-anchor=cheriot.demo.h --IPv6=n --unique-id=SCIHouse +xmake +xmake run + diff --git a/smartmeter/server_config/dnsmasq.conf b/smartmeter/server_config/dnsmasq.conf new file mode 100644 index 0000000..9a85708 --- /dev/null +++ b/smartmeter/server_config/dnsmasq.conf @@ -0,0 +1,691 @@ +# Configuration file for dnsmasq. +# +# Format is one option per line, legal options are the same +# as the long options legal on the command line. See +# "/opt/homebrew/sbin/dnsmasq --help" or "man 8 dnsmasq" for details. + +# Listen on this specific port instead of the standard DNS port +# (53). Setting this to zero completely disables DNS function, +# leaving only DHCP and/or TFTP. +#port=5353 + +# The following two options make you a better netizen, since they +# tell dnsmasq to filter out queries which the public DNS cannot +# answer, and which load the servers (especially the root servers) +# unnecessarily. If you have a dial-on-demand link they also stop +# these requests from bringing up the link unnecessarily. + +# Never forward plain names (without a dot or domain part) +#domain-needed +# Never forward addresses in the non-routed address spaces. +#bogus-priv + +# Uncomment these to enable DNSSEC validation and caching: +# (Requires dnsmasq to be built with DNSSEC option.) +#conf-file=%%PREFIX%%/share/dnsmasq/trust-anchors.conf +#dnssec + +# Replies which are not DNSSEC signed may be legitimate, because the domain +# is unsigned, or may be forgeries. Setting this option tells dnsmasq to +# check that an unsigned reply is OK, by finding a secure proof that a DS +# record somewhere between the root and the domain does not exist. +# The cost of setting this is that even queries in unsigned domains will need +# one or more extra DNS queries to verify. +#dnssec-check-unsigned + +# Uncomment this to filter useless windows-originated DNS requests +# which can trigger dial-on-demand links needlessly. +# Note that (amongst other things) this blocks all SRV requests, +# so don't use it if you use eg Kerberos, SIP, XMMP or Google-talk. +# This option only affects forwarding, SRV records originating for +# dnsmasq (via srv-host= lines) are not suppressed by it. +#filterwin2k + +# Change this line if you want dns to get its upstream servers from +# somewhere other that /etc/resolv.conf +#resolv-file= + +# By default, dnsmasq will send queries to any of the upstream +# servers it knows about and tries to favour servers to are known +# to be up. Uncommenting this forces dnsmasq to try each query +# with each server strictly in the order they appear in +# /etc/resolv.conf +#strict-order + +# If you don't want dnsmasq to read /etc/resolv.conf or any other +# file, getting its servers from this file instead (see below), then +# uncomment this. +#no-resolv + +# If you don't want dnsmasq to poll /etc/resolv.conf or other resolv +# files for changes and re-read them then uncomment this. +#no-poll + +# Add other name servers here, with domain specs if they are for +# non-public domains. +#server=/localnet/192.168.0.1 + +# Example of routing PTR queries to nameservers: this will send all +# address->name queries for 192.168.3/24 to nameserver 10.1.2.3 +#server=/3.168.192.in-addr.arpa/10.1.2.3 + +# Add local-only domains here, queries in these domains are answered +# from /etc/hosts or DHCP only. +local=/cheriot.demo/ + +# Add domains which you want to force to an IP address here. +# The example below send any host in double-click.net to a local +# web-server. +#address=/double-click.net/127.0.0.1 +address=/pool.ntp.org/10.0.0.10 +address=/cheriot.demo/10.0.0.10 + +# --address (and --server) work with IPv6 addresses too. +#address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 + +# Add the IPs of all queries to yahoo.com, google.com, and their +# subdomains to the vpn and search ipsets: +#ipset=/yahoo.com/google.com/vpn,search + +# Add the IPs of all queries to yahoo.com, google.com, and their +# subdomains to netfilters sets, which is equivalent to +# 'nft add element ip test vpn { ... }; nft add element ip test search { ... }' +#nftset=/yahoo.com/google.com/ip#test#vpn,ip#test#search + +# Use netfilters sets for both IPv4 and IPv6: +# This adds all addresses in *.yahoo.com to vpn4 and vpn6 for IPv4 and IPv6 addresses. +#nftset=/yahoo.com/4#ip#test#vpn4 +#nftset=/yahoo.com/6#ip#test#vpn6 + +# You can control how dnsmasq talks to a server: this forces +# queries to 10.1.2.3 to be routed via eth1 +# server=10.1.2.3@eth1 + +# and this sets the source (ie local) address used to talk to +# 10.1.2.3 to 192.168.1.1 port 55 (there must be an interface with that +# IP on the machine, obviously). +# server=10.1.2.3@192.168.1.1#55 + +# If you want dnsmasq to change uid and gid to something other +# than the default, edit the following lines. +#user= +#group= + +# If you want dnsmasq to listen for DHCP and DNS requests only on +# specified interfaces (and the loopback) give the name of the +# interface (eg eth0) here. +# Repeat the line for more than one interface. +interface=en9 +# Or you can specify which interface _not_ to listen on +#except-interface= +# Or which to listen on by address (remember to include 127.0.0.1 if +# you use this.) +#listen-address= +# If you want dnsmasq to provide only DNS service on an interface, +# configure it as shown above, and then use the following line to +# disable DHCP and TFTP on it. +#no-dhcp-interface= + +# On systems which support it, dnsmasq binds the wildcard address, +# even when it is listening on only some interfaces. It then discards +# requests that it shouldn't reply to. This has the advantage of +# working even when interfaces come and go and change address. If you +# want dnsmasq to really bind only the interfaces it is listening on, +# uncomment this option. About the only time you may need this is when +# running another nameserver on the same machine. +#bind-interfaces + +# If you don't want dnsmasq to read /etc/hosts, uncomment the +# following line. +#no-hosts +# or if you want it to read another file, as well as /etc/hosts, use +# this. +#addn-hosts=/etc/banner_add_hosts + +# Set this (and domain: see below) if you want to have a domain +# automatically added to simple names in a hosts-file. +#expand-hosts + +# Set the domain for dnsmasq. this is optional, but if it is set, it +# does the following things. +# 1) Allows DHCP hosts to have fully qualified domain names, as long +# as the domain part matches this setting. +# 2) Sets the "domain" DHCP option thereby potentially setting the +# domain of all systems configured by DHCP +# 3) Provides the domain part for "expand-hosts" +#domain=thekelleys.org.uk + +# Set a different domain for a particular subnet +#domain=wireless.thekelleys.org.uk,192.168.2.0/24 + +# Same idea, but range rather then subnet +#domain=reserved.thekelleys.org.uk,192.68.3.100,192.168.3.200 + +# Uncomment this to enable the integrated DHCP server, you need +# to supply the range of addresses available for lease and optionally +# a lease time. If you have more than one network, you will need to +# repeat this for each network on which you want to supply DHCP +# service. +dhcp-range=10.0.0.11,10.255.255.254,12h + +# This is an example of a DHCP range where the netmask is given. This +# is needed for networks we reach the dnsmasq DHCP server via a relay +# agent. If you don't know what a DHCP relay agent is, you probably +# don't need to worry about this. +#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h + +# This is an example of a DHCP range which sets a tag, so that +# some DHCP options may be set only for this network. +#dhcp-range=set:red,192.168.0.50,192.168.0.150 + +# Use this DHCP range only when the tag "green" is set. +#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h + +# Specify a subnet which can't be used for dynamic address allocation, +# is available for hosts with matching --dhcp-host lines. Note that +# dhcp-host declarations will be ignored unless there is a dhcp-range +# of some type for the subnet in question. +# In this case the netmask is implied (it comes from the network +# configuration on the machine running dnsmasq) it is possible to give +# an explicit netmask instead. +#dhcp-range=192.168.0.0,static + +# Enable DHCPv6. Note that the prefix-length does not need to be specified +# and defaults to 64 if missing/ +#dhcp-range=1234::2, 1234::500, 64, 12h + +# Do Router Advertisements, BUT NOT DHCP for this subnet. +#dhcp-range=1234::, ra-only + +# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and +# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack +# hosts. Use the DHCPv4 lease to derive the name, network segment and +# MAC address and assume that the host will also have an +# IPv6 address calculated using the SLAAC algorithm. +#dhcp-range=1234::, ra-names + +# Do Router Advertisements, BUT NOT DHCP for this subnet. +# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.) +#dhcp-range=1234::, ra-only, 48h + +# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA +# so that clients can use SLAAC addresses as well as DHCP ones. +#dhcp-range=1234::2, 1234::500, slaac + +# Do Router Advertisements and stateless DHCP for this subnet. Clients will +# not get addresses from DHCP, but they will get other configuration information. +# They will use SLAAC for addresses. +#dhcp-range=1234::, ra-stateless + +# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses +# from DHCPv4 leases. +#dhcp-range=1234::, ra-stateless, ra-names + +# Do router advertisements for all subnets where we're doing DHCPv6 +# Unless overridden by ra-stateless, ra-names, et al, the router +# advertisements will have the M and O bits set, so that the clients +# get addresses and configuration from DHCPv6, and the A bit reset, so the +# clients don't use SLAAC addresses. +#enable-ra + +# Supply parameters for specified hosts using DHCP. There are lots +# of valid alternatives, so we will give examples of each. Note that +# IP addresses DO NOT have to be in the range given above, they just +# need to be on the same network. The order of the parameters in these +# do not matter, it's permissible to give name, address and MAC in any +# order. + +# Always allocate the host with Ethernet address 11:22:33:44:55:66 +# The IP address 192.168.0.60 +#dhcp-host=11:22:33:44:55:66,192.168.0.60 + +# Always set the name of the host with hardware address +# 11:22:33:44:55:66 to be "fred" +#dhcp-host=11:22:33:44:55:66,fred + +# Always give the host with Ethernet address 11:22:33:44:55:66 +# the name fred and IP address 192.168.0.60 and lease time 45 minutes +#dhcp-host=11:22:33:44:55:66,fred,192.168.0.60,45m + +# Give a host with Ethernet address 11:22:33:44:55:66 or +# 12:34:56:78:90:12 the IP address 192.168.0.60. Dnsmasq will assume +# that these two Ethernet interfaces will never be in use at the same +# time, and give the IP address to the second, even if it is already +# in use by the first. Useful for laptops with wired and wireless +# addresses. +#dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60 + +# Give the machine which says its name is "bert" IP address +# 192.168.0.70 and an infinite lease +#dhcp-host=bert,192.168.0.70,infinite + +# Always give the host with client identifier 01:02:02:04 +# the IP address 192.168.0.60 +#dhcp-host=id:01:02:02:04,192.168.0.60 + +# Always give the InfiniBand interface with hardware address +# 80:00:00:48:fe:80:00:00:00:00:00:00:f4:52:14:03:00:28:05:81 the +# ip address 192.168.0.61. The client id is derived from the prefix +# ff:00:00:00:00:00:02:00:00:02:c9:00 and the last 8 pairs of +# hex digits of the hardware address. +#dhcp-host=id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61 + +# Always give the host with client identifier "marjorie" +# the IP address 192.168.0.60 +#dhcp-host=id:marjorie,192.168.0.60 + +# Enable the address given for "judge" in /etc/hosts +# to be given to a machine presenting the name "judge" when +# it asks for a DHCP lease. +#dhcp-host=judge + +# Never offer DHCP service to a machine whose Ethernet +# address is 11:22:33:44:55:66 +#dhcp-host=11:22:33:44:55:66,ignore + +# Ignore any client-id presented by the machine with Ethernet +# address 11:22:33:44:55:66. This is useful to prevent a machine +# being treated differently when running under different OS's or +# between PXE boot and OS boot. +#dhcp-host=11:22:33:44:55:66,id:* + +# Send extra options which are tagged as "red" to +# the machine with Ethernet address 11:22:33:44:55:66 +#dhcp-host=11:22:33:44:55:66,set:red + +# Send extra options which are tagged as "red" to +# any machine with Ethernet address starting 11:22:33: +#dhcp-host=11:22:33:*:*:*,set:red + +# Give a fixed IPv6 address and name to client with +# DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 +# Note the MAC addresses CANNOT be used to identify DHCPv6 clients. +# Note also that the [] around the IPv6 address are obligatory. +#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] + +# Ignore any clients which are not specified in dhcp-host lines +# or /etc/ethers. Equivalent to ISC "deny unknown-clients". +# This relies on the special "known" tag which is set when +# a host is matched. +#dhcp-ignore=tag:!known + +# Send extra options which are tagged as "red" to any machine whose +# DHCP vendorclass string includes the substring "Linux" +#dhcp-vendorclass=set:red,Linux + +# Send extra options which are tagged as "red" to any machine one +# of whose DHCP userclass strings includes the substring "accounts" +#dhcp-userclass=set:red,accounts + +# Send extra options which are tagged as "red" to any machine whose +# MAC address matches the pattern. +#dhcp-mac=set:red,00:60:8C:*:*:* + +# If this line is uncommented, dnsmasq will read /etc/ethers and act +# on the ethernet-address/IP pairs found there just as if they had +# been given as --dhcp-host options. Useful if you keep +# MAC-address/host mappings there for other purposes. +#read-ethers + +# Send options to hosts which ask for a DHCP lease. +# See RFC 2132 for details of available options. +# Common options can be given to dnsmasq by name: +# run "dnsmasq --help dhcp" to get a list. +# Note that all the common settings, such as netmask and +# broadcast address, DNS server and default route, are given +# sane defaults by dnsmasq. You very likely will not need +# any dhcp-options. If you use Windows clients and Samba, there +# are some options which are recommended, they are detailed at the +# end of this section. + +# Override the default route supplied by dnsmasq, which assumes the +# router is the same machine as the one running dnsmasq. +#dhcp-option=3,1.2.3.4 + +# Do the same thing, but using the option name +#dhcp-option=option:router,1.2.3.4 + +# Override the default route supplied by dnsmasq and send no default +# route at all. Note that this only works for the options sent by +# default (1, 3, 6, 12, 28) the same line will send a zero-length option +# for all other option numbers. +#dhcp-option=3 + +# Set the NTP time server addresses to 192.168.0.4 and 10.10.0.5 +#dhcp-option=option:ntp-server,192.168.0.4,10.10.0.5 + +# Send DHCPv6 option. Note [] around IPv6 addresses. +#dhcp-option=option6:dns-server,[1234::77],[1234::88] + +# Send DHCPv6 option for namservers as the machine running +# dnsmasq and another. +#dhcp-option=option6:dns-server,[::],[1234::88] + +# Ask client to poll for option changes every six hours. (RFC4242) +#dhcp-option=option6:information-refresh-time,6h + +# Set option 58 client renewal time (T1). Defaults to half of the +# lease time if not specified. (RFC2132) +#dhcp-option=option:T1,1m + +# Set option 59 rebinding time (T2). Defaults to 7/8 of the +# lease time if not specified. (RFC2132) +#dhcp-option=option:T2,2m + +# Set the NTP time server address to be the same machine as +# is running dnsmasq +#dhcp-option=42,0.0.0.0 + +# Set the NIS domain name to "welly" +#dhcp-option=40,welly + +# Set the default time-to-live to 50 +#dhcp-option=23,50 + +# Set the "all subnets are local" flag +#dhcp-option=27,1 + +# Send the etherboot magic flag and then etherboot options (a string). +#dhcp-option=128,e4:45:74:68:00:00 +#dhcp-option=129,NIC=eepro100 + +# Specify an option which will only be sent to the "red" network +# (see dhcp-range for the declaration of the "red" network) +# Note that the tag: part must precede the option: part. +#dhcp-option = tag:red, option:ntp-server, 192.168.1.1 + +# The following DHCP options set up dnsmasq in the same way as is specified +# for the ISC dhcpcd in +# https://web.archive.org/web/20040313070105/http://us1.samba.org/samba/ftp/docs/textdocs/DHCP-Server-Configuration.txt +# adapted for a typical dnsmasq installation where the host running +# dnsmasq is also the host running samba. +# you may want to uncomment some or all of them if you use +# Windows clients and Samba. +#dhcp-option=19,0 # option ip-forwarding off +#dhcp-option=44,0.0.0.0 # set netbios-over-TCP/IP nameserver(s) aka WINS server(s) +#dhcp-option=45,0.0.0.0 # netbios datagram distribution server +#dhcp-option=46,8 # netbios node type + +# Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave. +#dhcp-option=252,"\n" + +# Send RFC-3397 DNS domain search DHCP option. WARNING: Your DHCP client +# probably doesn't support this...... +#dhcp-option=option:domain-search,eng.apple.com,marketing.apple.com + +# Send RFC-3442 classless static routes (note the netmask encoding) +#dhcp-option=121,192.168.1.0/24,1.2.3.4,10.0.0.0/8,5.6.7.8 + +# Send vendor-class specific options encapsulated in DHCP option 43. +# The meaning of the options is defined by the vendor-class so +# options are sent only when the client supplied vendor class +# matches the class given here. (A substring match is OK, so "MSFT" +# matches "MSFT" and "MSFT 5.0"). This example sets the +# mtftp address to 0.0.0.0 for PXEClients. +#dhcp-option=vendor:PXEClient,1,0.0.0.0 + +# Send microsoft-specific option to tell windows to release the DHCP lease +# when it shuts down. Note the "i" flag, to tell dnsmasq to send the +# value as a four-byte integer - that's what microsoft wants. See +# http://technet2.microsoft.com/WindowsServer/en/library/a70f1bb7-d2d4-49f0-96d6-4b7414ecfaae1033.mspx?mfr=true +#dhcp-option=vendor:MSFT,2,1i + +# Send the Encapsulated-vendor-class ID needed by some configurations of +# Etherboot to allow is to recognise the DHCP server. +#dhcp-option=vendor:Etherboot,60,"Etherboot" + +# Send options to PXELinux. Note that we need to send the options even +# though they don't appear in the parameter request list, so we need +# to use dhcp-option-force here. +# See http://syslinux.zytor.com/pxe.php#special for details. +# Magic number - needed before anything else is recognised +#dhcp-option-force=208,f1:00:74:7e +# Configuration file name +#dhcp-option-force=209,configs/common +# Path prefix +#dhcp-option-force=210,/tftpboot/pxelinux/files/ +# Reboot time. (Note 'i' to send 32-bit value) +#dhcp-option-force=211,30i + +# Set the boot filename for netboot/PXE. You will only need +# this if you want to boot machines over the network and you will need +# a TFTP server; either dnsmasq's built-in TFTP server or an +# external one. (See below for how to enable the TFTP server.) +#dhcp-boot=pxelinux.0 + +# The same as above, but use custom tftp-server instead machine running dnsmasq +#dhcp-boot=pxelinux,server.name,192.168.1.100 + +# Boot for iPXE. The idea is to send two different +# filenames, the first loads iPXE, and the second tells iPXE what to +# load. The dhcp-match sets the ipxe tag for requests from iPXE. +#dhcp-boot=undionly.kpxe +#dhcp-match=set:ipxe,175 # iPXE sends a 175 option. +#dhcp-boot=tag:ipxe,http://boot.ipxe.org/demo/boot.php + +# Encapsulated options for iPXE. All the options are +# encapsulated within option 175 +#dhcp-option=encap:175, 1, 5b # priority code +#dhcp-option=encap:175, 176, 1b # no-proxydhcp +#dhcp-option=encap:175, 177, string # bus-id +#dhcp-option=encap:175, 189, 1b # BIOS drive code +#dhcp-option=encap:175, 190, user # iSCSI username +#dhcp-option=encap:175, 191, pass # iSCSI password + +# Test for the architecture of a netboot client. PXE clients are +# supposed to send their architecture as option 93. (See RFC 4578) +#dhcp-match=peecees, option:client-arch, 0 #x86-32 +#dhcp-match=itanics, option:client-arch, 2 #IA64 +#dhcp-match=hammers, option:client-arch, 6 #x86-64 +#dhcp-match=mactels, option:client-arch, 7 #EFI x86-64 + +# Do real PXE, rather than just booting a single file, this is an +# alternative to dhcp-boot. +#pxe-prompt="What system shall I netboot?" +# or with timeout before first available action is taken: +#pxe-prompt="Press F8 for menu.", 60 + +# Available boot services. for PXE. +#pxe-service=x86PC, "Boot from local disk" + +# Loads /pxelinux.0 from dnsmasq TFTP server. +#pxe-service=x86PC, "Install Linux", pxelinux + +# Loads /pxelinux.0 from TFTP server at 1.2.3.4. +# Beware this fails on old PXE ROMS. +#pxe-service=x86PC, "Install Linux", pxelinux, 1.2.3.4 + +# Use bootserver on network, found my multicast or broadcast. +#pxe-service=x86PC, "Install windows from RIS server", 1 + +# Use bootserver at a known IP address. +#pxe-service=x86PC, "Install windows from RIS server", 1, 1.2.3.4 + +# If you have multicast-FTP available, +# information for that can be passed in a similar way using options 1 +# to 5. See page 19 of +# http://download.intel.com/design/archives/wfm/downloads/pxespec.pdf + + +# Enable dnsmasq's built-in TFTP server +#enable-tftp + +# Set the root directory for files available via FTP. +#tftp-root=/var/ftpd + +# Do not abort if the tftp-root is unavailable +#tftp-no-fail + +# Make the TFTP server more secure: with this set, only files owned by +# the user dnsmasq is running as will be send over the net. +#tftp-secure + +# This option stops dnsmasq from negotiating a larger blocksize for TFTP +# transfers. It will slow things down, but may rescue some broken TFTP +# clients. +#tftp-no-blocksize + +# Set the boot file name only when the "red" tag is set. +#dhcp-boot=tag:red,pxelinux.red-net + +# An example of dhcp-boot with an external TFTP server: the name and IP +# address of the server are given after the filename. +# Can fail with old PXE ROMS. Overridden by --pxe-service. +#dhcp-boot=/var/ftpd/pxelinux.0,boothost,192.168.0.3 + +# If there are multiple external tftp servers having a same name +# (using /etc/hosts) then that name can be specified as the +# tftp_servername (the third option to dhcp-boot) and in that +# case dnsmasq resolves this name and returns the resultant IP +# addresses in round robin fashion. This facility can be used to +# load balance the tftp load among a set of servers. +#dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name + +# Set the limit on DHCP leases, the default is 150 +#dhcp-lease-max=150 + +# The DHCP server needs somewhere on disk to keep its lease database. +# This defaults to a sane location, but if you want to change it, use +# the line below. +#dhcp-leasefile=/opt/homebrew/var/lib/misc/dnsmasq/dnsmasq.leases + +# Set the DHCP server to authoritative mode. In this mode it will barge in +# and take over the lease for any client which broadcasts on the network, +# whether it has a record of the lease or not. This avoids long timeouts +# when a machine wakes up on a new network. DO NOT enable this if there's +# the slightest chance that you might end up accidentally configuring a DHCP +# server for your campus/company accidentally. The ISC server uses +# the same option, and this URL provides more information: +# http://www.isc.org/files/auth.html +#dhcp-authoritative + +# Set the DHCP server to enable DHCPv4 Rapid Commit Option per RFC 4039. +# In this mode it will respond to a DHCPDISCOVER message including a Rapid Commit +# option with a DHCPACK including a Rapid Commit option and fully committed address +# and configuration information. This must only be enabled if either the server is +# the only server for the subnet, or multiple servers are present and they each +# commit a binding for all clients. +#dhcp-rapid-commit + +# Run an executable when a DHCP lease is created or destroyed. +# The arguments sent to the script are "add" or "del", +# then the MAC address, the IP address and finally the hostname +# if there is one. +#dhcp-script=/bin/echo + +# Set the cachesize here. +#cache-size=150 + +# If you want to disable negative caching, uncomment this. +#no-negcache + +# Normally responses which come from /etc/hosts and the DHCP lease +# file have Time-To-Live set as zero, which conventionally means +# do not cache further. If you are happy to trade lower load on the +# server for potentially stale date, you can set a time-to-live (in +# seconds) here. +#local-ttl= + +# If you want dnsmasq to detect attempts by Verisign to send queries +# to unregistered .com and .net hosts to its sitefinder service and +# have dnsmasq instead return the correct NXDOMAIN response, uncomment +# this line. You can add similar lines to do the same for other +# registries which have implemented wildcard A records. +#bogus-nxdomain=64.94.110.11 + +# If you want to fix up DNS results from upstream servers, use the +# alias option. This only works for IPv4. +# This alias makes a result of 1.2.3.4 appear as 5.6.7.8 +#alias=1.2.3.4,5.6.7.8 +# and this maps 1.2.3.x to 5.6.7.x +#alias=1.2.3.0,5.6.7.0,255.255.255.0 +# and this maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40 +#alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0 + +# Change these lines if you want dnsmasq to serve MX records. + +# Return an MX record named "maildomain.com" with target +# servermachine.com and preference 50 +#mx-host=maildomain.com,servermachine.com,50 + +# Set the default target for MX records created using the localmx option. +#mx-target=servermachine.com + +# Return an MX record pointing to the mx-target for all local +# machines. +#localmx + +# Return an MX record pointing to itself for all local machines. +#selfmx + +# Change the following lines if you want dnsmasq to serve SRV +# records. These are useful if you want to serve ldap requests for +# Active Directory and other windows-originated DNS requests. +# See RFC 2782. +# You may add multiple srv-host lines. +# The fields are ,,,, +# If the domain part if missing from the name (so that is just has the +# service and protocol sections) then the domain given by the domain= +# config option is used. (Note that expand-hosts does not need to be +# set for this to work.) + +# A SRV record sending LDAP for the example.com domain to +# ldapserver.example.com port 389 +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389 + +# A SRV record sending LDAP for the example.com domain to +# ldapserver.example.com port 389 (using domain=) +#domain=example.com +#srv-host=_ldap._tcp,ldapserver.example.com,389 + +# Two SRV records for LDAP, each with different priorities +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,1 +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,2 + +# A SRV record indicating that there is no LDAP server for the domain +# example.com +#srv-host=_ldap._tcp.example.com + +# The following line shows how to make dnsmasq serve an arbitrary PTR +# record. This is useful for DNS-SD. (Note that the +# domain-name expansion done for SRV records _does_not +# occur for PTR records.) +#ptr-record=_http._tcp.dns-sd-services,"New Employee Page._http._tcp.dns-sd-services" + +# Change the following lines to enable dnsmasq to serve TXT records. +# These are used for things like SPF and zeroconf. (Note that the +# domain-name expansion done for SRV records _does_not +# occur for TXT records.) + +#Example SPF. +#txt-record=example.com,"v=spf1 a -all" + +#Example zeroconf +#txt-record=_http._tcp.example.com,name=value,paper=A4 + +# Provide an alias for a "local" DNS name. Note that this _only_ works +# for targets which are names from DHCP or /etc/hosts. Give host +# "bert" another name, bertrand +#cname=bertrand,bert + +# For debugging purposes, log each DNS query as it passes through +# dnsmasq. +#log-queries + +# Log lots of extra information about DHCP transactions. +#log-dhcp + +# Include another lot of configuration options. +#conf-file=/etc/dnsmasq.more.conf +#conf-dir=/opt/homebrew/etc/dnsmasq.d + +# Include all the files in a directory except those ending in .bak +#conf-dir=/opt/homebrew/etc/dnsmasq.d,.bak + +# Include all files in a directory which end in .conf +#conf-dir=/opt/homebrew/etc/dnsmasq.d/,*.conf + +# If a DHCP client claims that its name is "wpad", ignore that. +# This fixes a security hole. see CERT Vulnerability VU#598349 +#dhcp-name-match=set:wpad-ignore,wpad +#dhcp-ignore-names=tag:wpad-ignore diff --git a/smartmeter/server_config/mosquitto.conf b/smartmeter/server_config/mosquitto.conf new file mode 100644 index 0000000..4c1bb61 --- /dev/null +++ b/smartmeter/server_config/mosquitto.conf @@ -0,0 +1,923 @@ +# Config file for mosquitto +# +# See mosquitto.conf(5) for more information. +# +# Default values are shown, uncomment to change. +# +# Use the # character to indicate a comment, but only if it is the +# very first character on the line. + +# ================================================================= +# General configuration +# ================================================================= + +# Use per listener security settings. +# +# It is recommended this option be set before any other options. +# +# If this option is set to true, then all authentication and access control +# options are controlled on a per listener basis. The following options are +# affected: +# +# acl_file +# allow_anonymous +# allow_zero_length_clientid +# auto_id_prefix +# password_file +# plugin +# plugin_opt_* +# psk_file +# +# Note that if set to true, then a durable client (i.e. with clean session set +# to false) that has disconnected will use the ACL settings defined for the +# listener that it was most recently connected to. +# +# The default behaviour is for this to be set to false, which maintains the +# setting behaviour from previous versions of mosquitto. +#per_listener_settings false + + +# This option controls whether a client is allowed to connect with a zero +# length client id or not. This option only affects clients using MQTT v3.1.1 +# and later. If set to false, clients connecting with a zero length client id +# are disconnected. If set to true, clients will be allocated a client id by +# the broker. This means it is only useful for clients with clean session set +# to true. +#allow_zero_length_clientid true + +# If allow_zero_length_clientid is true, this option allows you to set a prefix +# to automatically generated client ids to aid visibility in logs. +# Defaults to 'auto-' +#auto_id_prefix auto- + +# This option affects the scenario when a client subscribes to a topic that has +# retained messages. It is possible that the client that published the retained +# message to the topic had access at the time they published, but that access +# has been subsequently removed. If check_retain_source is set to true, the +# default, the source of a retained message will be checked for access rights +# before it is republished. When set to false, no check will be made and the +# retained message will always be published. This affects all listeners. +#check_retain_source true + +# QoS 1 and 2 messages will be allowed inflight per client until this limit +# is exceeded. Defaults to 0. (No maximum) +# See also max_inflight_messages +#max_inflight_bytes 0 + +# The maximum number of QoS 1 and 2 messages currently inflight per +# client. +# This includes messages that are partway through handshakes and +# those that are being retried. Defaults to 20. Set to 0 for no +# maximum. Setting to 1 will guarantee in-order delivery of QoS 1 +# and 2 messages. +#max_inflight_messages 20 + +# For MQTT v5 clients, it is possible to have the server send a "server +# keepalive" value that will override the keepalive value set by the client. +# This is intended to be used as a mechanism to say that the server will +# disconnect the client earlier than it anticipated, and that the client should +# use the new keepalive value. The max_keepalive option allows you to specify +# that clients may only connect with keepalive less than or equal to this +# value, otherwise they will be sent a server keepalive telling them to use +# max_keepalive. This only applies to MQTT v5 clients. The default, and maximum +# value allowable, is 65535. +# +# Set to 0 to allow clients to set keepalive = 0, which means no keepalive +# checks are made and the client will never be disconnected by the broker if no +# messages are received. You should be very sure this is the behaviour that you +# want. +# +# For MQTT v3.1.1 and v3.1 clients, there is no mechanism to tell the client +# what keepalive value they should use. If an MQTT v3.1.1 or v3.1 client +# specifies a keepalive time greater than max_keepalive they will be sent a +# CONNACK message with the "identifier rejected" reason code, and disconnected. +# +#max_keepalive 65535 + +# For MQTT v5 clients, it is possible to have the server send a "maximum packet +# size" value that will instruct the client it will not accept MQTT packets +# with size greater than max_packet_size bytes. This applies to the full MQTT +# packet, not just the payload. Setting this option to a positive value will +# set the maximum packet size to that number of bytes. If a client sends a +# packet which is larger than this value, it will be disconnected. This applies +# to all clients regardless of the protocol version they are using, but v3.1.1 +# and earlier clients will of course not have received the maximum packet size +# information. Defaults to no limit. Setting below 20 bytes is forbidden +# because it is likely to interfere with ordinary client operation, even with +# very small payloads. +#max_packet_size 0 + +# QoS 1 and 2 messages above those currently in-flight will be queued per +# client until this limit is exceeded. Defaults to 0. (No maximum) +# See also max_queued_messages. +# If both max_queued_messages and max_queued_bytes are specified, packets will +# be queued until the first limit is reached. +#max_queued_bytes 0 + +# Set the maximum QoS supported. Clients publishing at a QoS higher than +# specified here will be disconnected. +#max_qos 2 + +# The maximum number of QoS 1 and 2 messages to hold in a queue per client +# above those that are currently in-flight. Defaults to 1000. Set +# to 0 for no maximum (not recommended). +# See also queue_qos0_messages. +# See also max_queued_bytes. +#max_queued_messages 1000 +# +# This option sets the maximum number of heap memory bytes that the broker will +# allocate, and hence sets a hard limit on memory use by the broker. Memory +# requests that exceed this value will be denied. The effect will vary +# depending on what has been denied. If an incoming message is being processed, +# then the message will be dropped and the publishing client will be +# disconnected. If an outgoing message is being sent, then the individual +# message will be dropped and the receiving client will be disconnected. +# Defaults to no limit. +#memory_limit 0 + +# This option sets the maximum publish payload size that the broker will allow. +# Received messages that exceed this size will not be accepted by the broker. +# The default value is 0, which means that all valid MQTT messages are +# accepted. MQTT imposes a maximum payload size of 268435455 bytes. +#message_size_limit 0 + +# This option allows the session of persistent clients (those with clean +# session set to false) that are not currently connected to be removed if they +# do not reconnect within a certain time frame. This is a non-standard option +# in MQTT v3.1. MQTT v3.1.1 and v5.0 allow brokers to remove client sessions. +# +# Badly designed clients may set clean session to false whilst using a randomly +# generated client id. This leads to persistent clients that connect once and +# never reconnect. This option allows these clients to be removed. This option +# allows persistent clients (those with clean session set to false) to be +# removed if they do not reconnect within a certain time frame. +# +# The expiration period should be an integer followed by one of h d w m y for +# hour, day, week, month and year respectively. For example +# +# persistent_client_expiration 2m +# persistent_client_expiration 14d +# persistent_client_expiration 1y +# +# The default if not set is to never expire persistent clients. +#persistent_client_expiration + +# Write process id to a file. Default is a blank string which means +# a pid file shouldn't be written. +# This should be set to /var/run/mosquitto/mosquitto.pid if mosquitto is +# being run automatically on boot with an init script and +# start-stop-daemon or similar. +#pid_file + +# Set to true to queue messages with QoS 0 when a persistent client is +# disconnected. These messages are included in the limit imposed by +# max_queued_messages and max_queued_bytes +# Defaults to false. +# This is a non-standard option for the MQTT v3.1 spec but is allowed in +# v3.1.1. +#queue_qos0_messages false + +# Set to false to disable retained message support. If a client publishes a +# message with the retain bit set, it will be disconnected if this is set to +# false. +#retain_available true + +# Disable Nagle's algorithm on client sockets. This has the effect of reducing +# latency of individual messages at the potential cost of increasing the number +# of packets being sent. +#set_tcp_nodelay false + +# Time in seconds between updates of the $SYS tree. +# Set to 0 to disable the publishing of the $SYS tree. +#sys_interval 10 + +# The MQTT specification requires that the QoS of a message delivered to a +# subscriber is never upgraded to match the QoS of the subscription. Enabling +# this option changes this behaviour. If upgrade_outgoing_qos is set true, +# messages sent to a subscriber will always match the QoS of its subscription. +# This is a non-standard option explicitly disallowed by the spec. +#upgrade_outgoing_qos false + +# When run as root, drop privileges to this user and its primary +# group. +# Set to root to stay as root, but this is not recommended. +# If set to "mosquitto", or left unset, and the "mosquitto" user does not exist +# then it will drop privileges to the "nobody" user instead. +# If run as a non-root user, this setting has no effect. +# Note that on Windows this has no effect and so mosquitto should be started by +# the user you wish it to run as. +#user mosquitto + +# ================================================================= +# Listeners +# ================================================================= + +# Listen on a port/ip address combination. By using this variable +# multiple times, mosquitto can listen on more than one port. If +# this variable is used and neither bind_address nor port given, +# then the default listener will not be started. +# The port number to listen on must be given. Optionally, an ip +# address or host name may be supplied as a second argument. In +# this case, mosquitto will attempt to bind the listener to that +# address and so restrict access to the associated network and +# interface. By default, mosquitto will listen on all interfaces. +# Note that for a websockets listener it is not possible to bind to a host +# name. +# +# On systems that support Unix Domain Sockets, it is also possible +# to create a # Unix socket rather than opening a TCP socket. In +# this case, the port number should be set to 0 and a unix socket +# path must be provided, e.g. +# listener 0 /tmp/mosquitto.sock +# +# listener port-number [ip address/host name/unix socket path] + +# unencrypted listener for local websockets +listener 8080 10.0.0.10 +protocol websockets +socket_domain ipv4 +log_type all +allow_anonymous true +connection_messages true + +# mqtt listener with self-signed demo cert and key +listener 8883 10.0.0.10 +protocol mqtt +tls_keyform pem +keyfile server_config/cheriot.demo.key +certfile frontends/static/cheriot.demo.crt +log_type all +allow_anonymous true +connection_messages true + + +# By default, a listener will attempt to listen on all supported IP protocol +# versions. If you do not have an IPv4 or IPv6 interface you may wish to +# disable support for either of those protocol versions. In particular, note +# that due to the limitations of the websockets library, it will only ever +# attempt to open IPv6 sockets if IPv6 support is compiled in, and so will fail +# if IPv6 is not available. +# +# Set to `ipv4` to force the listener to only use IPv4, or set to `ipv6` to +# force the listener to only use IPv6. If you want support for both IPv4 and +# IPv6, then do not use the socket_domain option. +# +#socket_domain + +# Bind the listener to a specific interface. This is similar to +# the [ip address/host name] part of the listener definition, but is useful +# when an interface has multiple addresses or the address may change. If used +# with the [ip address/host name] part of the listener definition, then the +# bind_interface option will take priority. +# Not available on Windows. +# +# Example: bind_interface eth0 +#bind_interface + +# When a listener is using the websockets protocol, it is possible to serve +# http data as well. Set http_dir to a directory which contains the files you +# wish to serve. If this option is not specified, then no normal http +# connections will be possible. +#http_dir + +# The maximum number of client connections to allow. This is +# a per listener setting. +# Default is -1, which means unlimited connections. +# Note that other process limits mean that unlimited connections +# are not really possible. Typically the default maximum number of +# connections possible is around 1024. +#max_connections -1 + +# The listener can be restricted to operating within a topic hierarchy using +# the mount_point option. This is achieved be prefixing the mount_point string +# to all topics for any clients connected to this listener. This prefixing only +# happens internally to the broker; the client will not see the prefix. +#mount_point + +# Choose the protocol to use when listening. +# This can be either mqtt or websockets. +# Certificate based TLS may be used with websockets, except that only the +# cafile, certfile, keyfile, ciphers, and ciphers_tls13 options are supported. +#protocol mqtt + +# Set use_username_as_clientid to true to replace the clientid that a client +# connected with with its username. This allows authentication to be tied to +# the clientid, which means that it is possible to prevent one client +# disconnecting another by using the same clientid. +# If a client connects with no username it will be disconnected as not +# authorised when this option is set to true. +# Do not use in conjunction with clientid_prefixes. +# See also use_identity_as_username. +# This does not apply globally, but on a per-listener basis. +#use_username_as_clientid + +# Change the websockets headers size. This is a global option, it is not +# possible to set per listener. This option sets the size of the buffer used in +# the libwebsockets library when reading HTTP headers. If you are passing large +# header data such as cookies then you may need to increase this value. If left +# unset, or set to 0, then the default of 1024 bytes will be used. +#websockets_headers_size + +# ----------------------------------------------------------------- +# Certificate based SSL/TLS support +# ----------------------------------------------------------------- +# The following options can be used to enable certificate based SSL/TLS support +# for this listener. Note that the recommended port for MQTT over TLS is 8883, +# but this must be set manually. +# +# See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS +# support" section. Only one of certificate or PSK encryption support can be +# enabled for any listener. + +# Both of certfile and keyfile must be defined to enable certificate based +# TLS encryption. + +# Path to the PEM encoded server certificate. +#certfile + +# Path to the PEM encoded keyfile. +#keyfile + +# If you wish to control which encryption ciphers are used, use the ciphers +# option. The list of available ciphers can be optained using the "openssl +# ciphers" command and should be provided in the same format as the output of +# that command. This applies to TLS 1.2 and earlier versions only. Use +# ciphers_tls1.3 for TLS v1.3. +#ciphers + +# Choose which TLS v1.3 ciphersuites are used for this listener. +# Defaults to "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256" +#ciphers_tls1.3 + +# If you have require_certificate set to true, you can create a certificate +# revocation list file to revoke access to particular client certificates. If +# you have done this, use crlfile to point to the PEM encoded revocation file. +#crlfile + +# To allow the use of ephemeral DH key exchange, which provides forward +# security, the listener must load DH parameters. This can be specified with +# the dhparamfile option. The dhparamfile can be generated with the command +# e.g. "openssl dhparam -out dhparam.pem 2048" +#dhparamfile + +# By default an TLS enabled listener will operate in a similar fashion to a +# https enabled web server, in that the server has a certificate signed by a CA +# and the client will verify that it is a trusted certificate. The overall aim +# is encryption of the network traffic. By setting require_certificate to true, +# the client must provide a valid certificate in order for the network +# connection to proceed. This allows access to the broker to be controlled +# outside of the mechanisms provided by MQTT. +#require_certificate false + +# cafile and capath define methods of accessing the PEM encoded +# Certificate Authority certificates that will be considered trusted when +# checking incoming client certificates. +# cafile defines the path to a file containing the CA certificates. +# capath defines a directory that will be searched for files +# containing the CA certificates. For capath to work correctly, the +# certificate files must have ".crt" as the file ending and you must run +# "openssl rehash " each time you add/remove a certificate. +# capath is not supported for websockets. +#cafile +#capath + + +# If require_certificate is true, you may set use_identity_as_username to true +# to use the CN value from the client certificate as a username. If this is +# true, the password_file option will not be used for this listener. +#use_identity_as_username false + +# ----------------------------------------------------------------- +# Pre-shared-key based SSL/TLS support +# ----------------------------------------------------------------- +# The following options can be used to enable PSK based SSL/TLS support for +# this listener. Note that the recommended port for MQTT over TLS is 8883, but +# this must be set manually. +# +# See also the mosquitto-tls man page and the "Certificate based SSL/TLS +# support" section. Only one of certificate or PSK encryption support can be +# enabled for any listener. + +# The psk_hint option enables pre-shared-key support for this listener and also +# acts as an identifier for this listener. The hint is sent to clients and may +# be used locally to aid authentication. The hint is a free form string that +# doesn't have much meaning in itself, so feel free to be creative. +# If this option is provided, see psk_file to define the pre-shared keys to be +# used or create a security plugin to handle them. +#psk_hint + +# When using PSK, the encryption ciphers used will be chosen from the list of +# available PSK ciphers. If you want to control which ciphers are available, +# use the "ciphers" option. The list of available ciphers can be optained +# using the "openssl ciphers" command and should be provided in the same format +# as the output of that command. +#ciphers + +# Set use_identity_as_username to have the psk identity sent by the client used +# as its username. Authentication will be carried out using the PSK rather than +# the MQTT username/password and so password_file will not be used for this +# listener. +#use_identity_as_username false + + +# ================================================================= +# Persistence +# ================================================================= + +# If persistence is enabled, save the in-memory database to disk +# every autosave_interval seconds. If set to 0, the persistence +# database will only be written when mosquitto exits. See also +# autosave_on_changes. +# Note that writing of the persistence database can be forced by +# sending mosquitto a SIGUSR1 signal. +#autosave_interval 1800 + +# If true, mosquitto will count the number of subscription changes, retained +# messages received and queued messages and if the total exceeds +# autosave_interval then the in-memory database will be saved to disk. +# If false, mosquitto will save the in-memory database to disk by treating +# autosave_interval as a time in seconds. +#autosave_on_changes false + +# Save persistent message data to disk (true/false). +# This saves information about all messages, including +# subscriptions, currently in-flight messages and retained +# messages. +# retained_persistence is a synonym for this option. +#persistence false + +# The filename to use for the persistent database, not including +# the path. +#persistence_file mosquitto.db + +# Location for persistent database. +# Default is an empty string (current directory). +# Set to e.g. /var/lib/mosquitto if running as a proper service on Linux or +# similar. +#persistence_location + + +# ================================================================= +# Logging +# ================================================================= + +# Places to log to. Use multiple log_dest lines for multiple +# logging destinations. +# Possible destinations are: stdout stderr syslog topic file dlt +# +# stdout and stderr log to the console on the named output. +# +# syslog uses the userspace syslog facility which usually ends up +# in /var/log/messages or similar. +# +# topic logs to the broker topic '$SYS/broker/log/', +# where severity is one of D, E, W, N, I, M which are debug, error, +# warning, notice, information and message. Message type severity is used by +# the subscribe/unsubscribe log_types and publishes log messages to +# $SYS/broker/log/M/susbcribe or $SYS/broker/log/M/unsubscribe. +# +# The file destination requires an additional parameter which is the file to be +# logged to, e.g. "log_dest file /var/log/mosquitto.log". The file will be +# closed and reopened when the broker receives a HUP signal. Only a single file +# destination may be configured. +# +# The dlt destination is for the automotive `Diagnostic Log and Trace` tool. +# This requires that Mosquitto has been compiled with DLT support. +# +# Note that if the broker is running as a Windows service it will default to +# "log_dest none" and neither stdout nor stderr logging is available. +# Use "log_dest none" if you wish to disable logging. +#log_dest stderr + +# Types of messages to log. Use multiple log_type lines for logging +# multiple types of messages. +# Possible types are: debug, error, warning, notice, information, +# none, subscribe, unsubscribe, websockets, all. +# Note that debug type messages are for decoding the incoming/outgoing +# network packets. They are not logged in "topics". +#log_type error +#log_type warning +#log_type notice +#log_type information + + +# If set to true, client connection and disconnection messages will be included +# in the log. +#connection_messages true + +# If using syslog logging (not on Windows), messages will be logged to the +# "daemon" facility by default. Use the log_facility option to choose which of +# local0 to local7 to log to instead. The option value should be an integer +# value, e.g. "log_facility 5" to use local5. +#log_facility + +# If set to true, add a timestamp value to each log message. +#log_timestamp true + +# Set the format of the log timestamp. If left unset, this is the number of +# seconds since the Unix epoch. +# This is a free text string which will be passed to the strftime function. To +# get an ISO 8601 datetime, for example: +# log_timestamp_format %Y-%m-%dT%H:%M:%S +#log_timestamp_format + +# Change the websockets logging level. This is a global option, it is not +# possible to set per listener. This is an integer that is interpreted by +# libwebsockets as a bit mask for its lws_log_levels enum. See the +# libwebsockets documentation for more details. "log_type websockets" must also +# be enabled. +#websockets_log_level 0 + + +# ================================================================= +# Security +# ================================================================= + +# If set, only clients that have a matching prefix on their +# clientid will be allowed to connect to the broker. By default, +# all clients may connect. +# For example, setting "secure-" here would mean a client "secure- +# client" could connect but another with clientid "mqtt" couldn't. +#clientid_prefixes + +# Boolean value that determines whether clients that connect +# without providing a username are allowed to connect. If set to +# false then a password file should be created (see the +# password_file option) to control authenticated client access. +# +# Defaults to false, unless there are no listeners defined in the configuration +# file, in which case it is set to true, but connections are only allowed from +# the local machine. +#allow_anonymous false + +# ----------------------------------------------------------------- +# Default authentication and topic access control +# ----------------------------------------------------------------- + +# Control access to the broker using a password file. This file can be +# generated using the mosquitto_passwd utility. If TLS support is not compiled +# into mosquitto (it is recommended that TLS support should be included) then +# plain text passwords are used, in which case the file should be a text file +# with lines in the format: +# username:password +# The password (and colon) may be omitted if desired, although this +# offers very little in the way of security. +# +# See the TLS client require_certificate and use_identity_as_username options +# for alternative authentication options. If a plugin is used as well as +# password_file, the plugin check will be made first. +#password_file + +# Access may also be controlled using a pre-shared-key file. This requires +# TLS-PSK support and a listener configured to use it. The file should be text +# lines in the format: +# identity:key +# The key should be in hexadecimal format without a leading "0x". +# If an plugin is used as well, the plugin check will be made first. +#psk_file + +# Control access to topics on the broker using an access control list +# file. If this parameter is defined then only the topics listed will +# have access. +# If the first character of a line of the ACL file is a # it is treated as a +# comment. +# Topic access is added with lines of the format: +# +# topic [read|write|readwrite|deny] +# +# The access type is controlled using "read", "write", "readwrite" or "deny". +# This parameter is optional (unless contains a space character) - if +# not given then the access is read/write. can contain the + or # +# wildcards as in subscriptions. +# +# The "deny" option can used to explicity deny access to a topic that would +# otherwise be granted by a broader read/write/readwrite statement. Any "deny" +# topics are handled before topics that grant read/write access. +# +# The first set of topics are applied to anonymous clients, assuming +# allow_anonymous is true. User specific topic ACLs are added after a +# user line as follows: +# +# user +# +# The username referred to here is the same as in password_file. It is +# not the clientid. +# +# +# If is also possible to define ACLs based on pattern substitution within the +# topic. The patterns available for substition are: +# +# %c to match the client id of the client +# %u to match the username of the client +# +# The substitution pattern must be the only text for that level of hierarchy. +# +# The form is the same as for the topic keyword, but using pattern as the +# keyword. +# Pattern ACLs apply to all users even if the "user" keyword has previously +# been given. +# +# If using bridges with usernames and ACLs, connection messages can be allowed +# with the following pattern: +# pattern write $SYS/broker/connection/%c/state +# +# pattern [read|write|readwrite] +# +# Example: +# +# pattern write sensor/%u/data +# +# If an plugin is used as well as acl_file, the plugin check will be +# made first. +#acl_file + +# ----------------------------------------------------------------- +# External authentication and topic access plugin options +# ----------------------------------------------------------------- + +# External authentication and access control can be supported with the +# plugin option. This is a path to a loadable plugin. See also the +# plugin_opt_* options described below. +# +# The plugin option can be specified multiple times to load multiple +# plugins. The plugins will be processed in the order that they are specified +# here. If the plugin option is specified alongside either of +# password_file or acl_file then the plugin checks will be made first. +# +# If the per_listener_settings option is false, the plugin will be apply to all +# listeners. If per_listener_settings is true, then the plugin will apply to +# the current listener being defined only. +# +# This option is also available as `auth_plugin`, but this use is deprecated +# and will be removed in the future. +# +#plugin + +# If the plugin option above is used, define options to pass to the +# plugin here as described by the plugin instructions. All options named +# using the format plugin_opt_* will be passed to the plugin, for example: +# +# This option is also available as `auth_opt_*`, but this use is deprecated +# and will be removed in the future. +# +# plugin_opt_db_host +# plugin_opt_db_port +# plugin_opt_db_username +# plugin_opt_db_password + + +# ================================================================= +# Bridges +# ================================================================= + +# A bridge is a way of connecting multiple MQTT brokers together. +# Create a new bridge using the "connection" option as described below. Set +# options for the bridges using the remaining parameters. You must specify the +# address and at least one topic to subscribe to. +# +# Each connection must have a unique name. +# +# The address line may have multiple host address and ports specified. See +# below in the round_robin description for more details on bridge behaviour if +# multiple addresses are used. Note that if you use an IPv6 address, then you +# are required to specify a port. +# +# The direction that the topic will be shared can be chosen by +# specifying out, in or both, where the default value is out. +# The QoS level of the bridged communication can be specified with the next +# topic option. The default QoS level is 0, to change the QoS the topic +# direction must also be given. +# +# The local and remote prefix options allow a topic to be remapped when it is +# bridged to/from the remote broker. This provides the ability to place a topic +# tree in an appropriate location. +# +# For more details see the mosquitto.conf man page. +# +# Multiple topics can be specified per connection, but be careful +# not to create any loops. +# +# If you are using bridges with cleansession set to false (the default), then +# you may get unexpected behaviour from incoming topics if you change what +# topics you are subscribing to. This is because the remote broker keeps the +# subscription for the old topic. If you have this problem, connect your bridge +# with cleansession set to true, then reconnect with cleansession set to false +# as normal. +#connection +#address [:] [[:]] +#topic [[[out | in | both] qos-level] local-prefix remote-prefix] + +# If you need to have the bridge connect over a particular network interface, +# use bridge_bind_address to tell the bridge which local IP address the socket +# should bind to, e.g. `bridge_bind_address 192.168.1.10` +#bridge_bind_address + +# If a bridge has topics that have "out" direction, the default behaviour is to +# send an unsubscribe request to the remote broker on that topic. This means +# that changing a topic direction from "in" to "out" will not keep receiving +# incoming messages. Sending these unsubscribe requests is not always +# desirable, setting bridge_attempt_unsubscribe to false will disable sending +# the unsubscribe request. +#bridge_attempt_unsubscribe true + +# Set the version of the MQTT protocol to use with for this bridge. Can be one +# of mqttv50, mqttv311 or mqttv31. Defaults to mqttv311. +#bridge_protocol_version mqttv311 + +# Set the clean session variable for this bridge. +# When set to true, when the bridge disconnects for any reason, all +# messages and subscriptions will be cleaned up on the remote +# broker. Note that with cleansession set to true, there may be a +# significant amount of retained messages sent when the bridge +# reconnects after losing its connection. +# When set to false, the subscriptions and messages are kept on the +# remote broker, and delivered when the bridge reconnects. +#cleansession false + +# Set the amount of time a bridge using the lazy start type must be idle before +# it will be stopped. Defaults to 60 seconds. +#idle_timeout 60 + +# Set the keepalive interval for this bridge connection, in +# seconds. +#keepalive_interval 60 + +# Set the clientid to use on the local broker. If not defined, this defaults to +# 'local.'. If you are bridging a broker to itself, it is important +# that local_clientid and clientid do not match. +#local_clientid + +# If set to true, publish notification messages to the local and remote brokers +# giving information about the state of the bridge connection. Retained +# messages are published to the topic $SYS/broker/connection//state +# unless the notification_topic option is used. +# If the message is 1 then the connection is active, or 0 if the connection has +# failed. +# This uses the last will and testament feature. +#notifications true + +# Choose the topic on which notification messages for this bridge are +# published. If not set, messages are published on the topic +# $SYS/broker/connection//state +#notification_topic + +# Set the client id to use on the remote end of this bridge connection. If not +# defined, this defaults to 'name.hostname' where name is the connection name +# and hostname is the hostname of this computer. +# This replaces the old "clientid" option to avoid confusion. "clientid" +# remains valid for the time being. +#remote_clientid + +# Set the password to use when connecting to a broker that requires +# authentication. This option is only used if remote_username is also set. +# This replaces the old "password" option to avoid confusion. "password" +# remains valid for the time being. +#remote_password + +# Set the username to use when connecting to a broker that requires +# authentication. +# This replaces the old "username" option to avoid confusion. "username" +# remains valid for the time being. +#remote_username + +# Set the amount of time a bridge using the automatic start type will wait +# until attempting to reconnect. +# This option can be configured to use a constant delay time in seconds, or to +# use a backoff mechanism based on "Decorrelated Jitter", which adds a degree +# of randomness to when the restart occurs. +# +# Set a constant timeout of 20 seconds: +# restart_timeout 20 +# +# Set backoff with a base (start value) of 10 seconds and a cap (upper limit) of +# 60 seconds: +# restart_timeout 10 30 +# +# Defaults to jitter with a base of 5 and cap of 30 +#restart_timeout 5 30 + +# If the bridge has more than one address given in the address/addresses +# configuration, the round_robin option defines the behaviour of the bridge on +# a failure of the bridge connection. If round_robin is false, the default +# value, then the first address is treated as the main bridge connection. If +# the connection fails, the other secondary addresses will be attempted in +# turn. Whilst connected to a secondary bridge, the bridge will periodically +# attempt to reconnect to the main bridge until successful. +# If round_robin is true, then all addresses are treated as equals. If a +# connection fails, the next address will be tried and if successful will +# remain connected until it fails +#round_robin false + +# Set the start type of the bridge. This controls how the bridge starts and +# can be one of three types: automatic, lazy and once. Note that RSMB provides +# a fourth start type "manual" which isn't currently supported by mosquitto. +# +# "automatic" is the default start type and means that the bridge connection +# will be started automatically when the broker starts and also restarted +# after a short delay (30 seconds) if the connection fails. +# +# Bridges using the "lazy" start type will be started automatically when the +# number of queued messages exceeds the number set with the "threshold" +# parameter. It will be stopped automatically after the time set by the +# "idle_timeout" parameter. Use this start type if you wish the connection to +# only be active when it is needed. +# +# A bridge using the "once" start type will be started automatically when the +# broker starts but will not be restarted if the connection fails. +#start_type automatic + +# Set the number of messages that need to be queued for a bridge with lazy +# start type to be restarted. Defaults to 10 messages. +# Must be less than max_queued_messages. +#threshold 10 + +# If try_private is set to true, the bridge will attempt to indicate to the +# remote broker that it is a bridge not an ordinary client. If successful, this +# means that loop detection will be more effective and that retained messages +# will be propagated correctly. Not all brokers support this feature so it may +# be necessary to set try_private to false if your bridge does not connect +# properly. +#try_private true + +# Some MQTT brokers do not allow retained messages. MQTT v5 gives a mechanism +# for brokers to tell clients that they do not support retained messages, but +# this is not possible for MQTT v3.1.1 or v3.1. If you need to bridge to a +# v3.1.1 or v3.1 broker that does not support retained messages, set the +# bridge_outgoing_retain option to false. This will remove the retain bit on +# all outgoing messages to that bridge, regardless of any other setting. +#bridge_outgoing_retain true + +# If you wish to restrict the size of messages sent to a remote bridge, use the +# bridge_max_packet_size option. This sets the maximum number of bytes for +# the total message, including headers and payload. +# Note that MQTT v5 brokers may provide their own maximum-packet-size property. +# In this case, the smaller of the two limits will be used. +# Set to 0 for "unlimited". +#bridge_max_packet_size 0 + + +# ----------------------------------------------------------------- +# Certificate based SSL/TLS support +# ----------------------------------------------------------------- +# Either bridge_cafile or bridge_capath must be defined to enable TLS support +# for this bridge. +# bridge_cafile defines the path to a file containing the +# Certificate Authority certificates that have signed the remote broker +# certificate. +# bridge_capath defines a directory that will be searched for files containing +# the CA certificates. For bridge_capath to work correctly, the certificate +# files must have ".crt" as the file ending and you must run "openssl rehash +# " each time you add/remove a certificate. +#bridge_cafile +#bridge_capath + + +# If the remote broker has more than one protocol available on its port, e.g. +# MQTT and WebSockets, then use bridge_alpn to configure which protocol is +# requested. Note that WebSockets support for bridges is not yet available. +#bridge_alpn + +# When using certificate based encryption, bridge_insecure disables +# verification of the server hostname in the server certificate. This can be +# useful when testing initial server configurations, but makes it possible for +# a malicious third party to impersonate your server through DNS spoofing, for +# example. Use this option in testing only. If you need to resort to using this +# option in a production environment, your setup is at fault and there is no +# point using encryption. +#bridge_insecure false + +# Path to the PEM encoded client certificate, if required by the remote broker. +#bridge_certfile + +# Path to the PEM encoded client private key, if required by the remote broker. +#bridge_keyfile + +# ----------------------------------------------------------------- +# PSK based SSL/TLS support +# ----------------------------------------------------------------- +# Pre-shared-key encryption provides an alternative to certificate based +# encryption. A bridge can be configured to use PSK with the bridge_identity +# and bridge_psk options. These are the client PSK identity, and pre-shared-key +# in hexadecimal format with no "0x". Only one of certificate and PSK based +# encryption can be used on one +# bridge at once. +#bridge_identity +#bridge_psk + + +# ================================================================= +# External config files +# ================================================================= + +# External configuration files may be included by using the +# include_dir option. This defines a directory that will be searched +# for config files. All files that end in '.conf' will be loaded as +# a configuration file. It is best to have this as the last option +# in the main file. This option will only be processed from the main +# configuration file. The directory specified must not contain the +# main configuration file. +# Files within include_dir will be loaded sorted in case-sensitive +# alphabetical order, with capital letters ordered first. If this option is +# given multiple times, all of the files from the first instance will be +# processed before the next instance. See the man page for examples. +#include_dir diff --git a/smartmeter/server_config/ntp.conf b/smartmeter/server_config/ntp.conf new file mode 100644 index 0000000..ca4b5f2 --- /dev/null +++ b/smartmeter/server_config/ntp.conf @@ -0,0 +1,9 @@ +# ntp config used on mac +pool 0.debian.pool.ntp.org iburst +pool 1.debian.pool.ntp.org iburst +pool 2.debian.pool.ntp.org iburst +pool 3.debian.pool.ntp.org iburst + +interface listen ipv4 10.0.0.10 +# tell NTP to work in offline mode after 10s without peers +tos orphan 15 orphanwait 10