Esp32 refactoring (#907)
* updated dal * refactor controller api * use serial/spi base class * more cleanup * esp32spi -> esp32 * fix panic * fix panic constants * move response to net * moving lowercase to pxt * more request to net * build fixes * refactor net class * using net instance * bump pxt * updated azureiot * add secrets api in seattings * move access point to net * more secrets support * moving to secrets api * more comments * don't throw error * adding panic message * use device vs program secrets * use device secrets for wifi * adding some kind of system meny for wifi * update key * builtin check * update name * handle misconfigured wifi * renaming ocnfig keys * debug messages * patch daldts * text update * fixing a few settings * fix merge logic * adding menu * export closeMenu * hide console * cutify pxt_panic * refactoring controller into net package * consolidate abstractions * refactoring * rename esp32-game to net-game * updated menu * slightly better format * adding docs * updated mac address
This commit is contained in:
Родитель
4b76d7e7f3
Коммит
ef457fcc73
|
@ -2,3 +2,7 @@
|
|||
|
||||
Azure MQTT communication layer.
|
||||
A port of https://github.com/rovale/micro-mqtt for MakeCode.
|
||||
|
||||
## Settings
|
||||
|
||||
The connection string should be stored in the "azureiot" secret in the settings.
|
|
@ -5,6 +5,8 @@ const enum AzureIotEvent {
|
|||
}
|
||||
|
||||
namespace azureiot {
|
||||
export const SECRETS_KEY = "azureiot"
|
||||
|
||||
export let logPriority = ConsolePriority.Silent;
|
||||
|
||||
type SMap<T> = { [s: string]: T; }
|
||||
|
@ -25,12 +27,10 @@ namespace azureiot {
|
|||
return _mqttClient;
|
||||
}
|
||||
|
||||
export let network: net.Net
|
||||
export let connString = ""
|
||||
|
||||
function createMQTTClient() {
|
||||
_messageBusId = control.allocateNotifyEvent(); // TODO
|
||||
|
||||
const connString = settings.programSecrets.readSecret(SECRETS_KEY, true);
|
||||
const connStringParts = parsePropertyBag(connString, ";");
|
||||
const iotHubHostName = connStringParts["HostName"];
|
||||
const deviceId = connStringParts["DeviceName"];
|
||||
|
@ -43,7 +43,7 @@ namespace azureiot {
|
|||
password: "SharedAccessSignature " + sasToken,
|
||||
clientId: deviceId
|
||||
}
|
||||
const c = new mqtt.Client(opts, network);
|
||||
const c = new mqtt.Client(opts);
|
||||
c.on('connected', () => {
|
||||
log("connected")
|
||||
control.raiseEvent(_messageBusId, AzureIotEvent.Connected)
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
],
|
||||
"public": true,
|
||||
"dependencies": {
|
||||
"mqtt": "file:../mqtt"
|
||||
"mqtt": "file:../mqtt",
|
||||
"settings": "file:../settings"
|
||||
},
|
||||
"testDependencies": {
|
||||
"esp32": "file:../esp32"
|
||||
}
|
||||
}
|
|
@ -1,46 +1,23 @@
|
|||
interface Secrets {
|
||||
connString: string;
|
||||
wifi: pxt.StringMap;
|
||||
}
|
||||
// this is to be overridden in a separate file
|
||||
let secrets: Secrets;
|
||||
function test() {
|
||||
|
||||
const log = console.log;
|
||||
const esp = new esp32spi.SPIController(pins.spi(),
|
||||
pins.D13, pins.D11, pins.D12, pins.D10, 1)
|
||||
pins.D13, pins.D11, pins.D12, pins.D10, 1);
|
||||
|
||||
if (esp.status != esp32spi.WL_IDLE_STATUS)
|
||||
if (esp.isIdle)
|
||||
return
|
||||
|
||||
log(`Firmware vers. ${esp.firmwareVersion}`)
|
||||
log(`MAC addr: ${esp.MACaddress.toHex()}`)
|
||||
log("Temp: " + esp.getTemperature())
|
||||
|
||||
const networks = esp.scanNetworks()
|
||||
|
||||
log(JSON.stringify(secrets.wifi))
|
||||
|
||||
for (const ap of networks)
|
||||
log(`\t${ap.ssid}\t\tRSSI: ${ap.rssi}`)
|
||||
|
||||
for (let k of Object.keys(secrets.wifi)) {
|
||||
if (networks.some(n => n.ssid == k)) {
|
||||
log("connecting to " + k)
|
||||
esp.connectAP(k, secrets.wifi[k])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!esp.isConnected) {
|
||||
if (!esp.connect()) {
|
||||
log("can't connect")
|
||||
return
|
||||
}
|
||||
|
||||
log("ping: " + esp.ping("bing.com"))
|
||||
|
||||
azureiot.network = new esp32spi.NetTLS()
|
||||
azureiot.connString = secrets.connString
|
||||
azureiot.connect()
|
||||
log("mqtt connected")
|
||||
|
||||
|
|
|
@ -100,6 +100,16 @@
|
|||
|
||||
#define CFG_ACCELEROMETER_SPACE 81
|
||||
|
||||
#define CFG_PIN_WIFI_MOSI 82
|
||||
#define CFG_PIN_WIFI_MISO 83
|
||||
#define CFG_PIN_WIFI_SCK 84
|
||||
#define CFG_PIN_WIFI_TX 85
|
||||
#define CFG_PIN_WIFI_RX 86
|
||||
#define CFG_PIN_WIFI_CS 87
|
||||
#define CFG_PIN_WIFI_BUSY 88
|
||||
#define CFG_PIN_WIFI_RESET 89
|
||||
#define CFG_PIN_WIFI_GPIO0 90
|
||||
|
||||
// default I2C address
|
||||
#define ACCELEROMETER_TYPE_LIS3DH 0x32
|
||||
#define ACCELEROMETER_TYPE_LIS3DH_ALT 0x30
|
||||
|
|
|
@ -12,6 +12,48 @@ namespace control {
|
|||
control.runInParallel(a);
|
||||
}
|
||||
|
||||
export const enum PXT_PANIC {
|
||||
CODAL_OOM = 20,
|
||||
GC_OOM = 21,
|
||||
GC_TOO_BIG_ALLOCATION = 22,
|
||||
CODAL_HEAP_ERROR = 30,
|
||||
CODAL_NULL_DEREFERENCE = 40,
|
||||
CODAL_USB_ERROR = 50,
|
||||
CODAL_HARDWARE_CONFIGURATION_ERROR = 90,
|
||||
|
||||
INVALID_BINARY_HEADER = 901,
|
||||
OUT_OF_BOUNDS = 902,
|
||||
REF_DELETED = 903,
|
||||
SIZE = 904,
|
||||
INVALID_VTABLE = 905,
|
||||
INTERNAL_ERROR = 906,
|
||||
NO_SUCH_CONFIG = 907,
|
||||
NO_SUCH_PIN = 908,
|
||||
INVALID_ARGUMENT = 909,
|
||||
MEMORY_LIMIT_EXCEEDED = 910,
|
||||
SCREEN_ERROR = 911,
|
||||
MISSING_PROPERTY = 912,
|
||||
INVALID_IMAGE = 913,
|
||||
CALLED_FROM_ISR = 914,
|
||||
HEAP_DUMPED = 915,
|
||||
STACK_OVERFLOW = 916,
|
||||
BLOCKING_TO_STRING = 917,
|
||||
VM_ERROR = 918,
|
||||
SETTINGS_CLEARED = 920,
|
||||
SETTINGS_OVERLOAD = 921,
|
||||
SETTINGS_SECRET_MISSING = 922,
|
||||
|
||||
CAST_FIRST = 980,
|
||||
CAST_FROM_UNDEFINED = 980,
|
||||
CAST_FROM_BOOLEAN = 981,
|
||||
CAST_FROM_NUMBER = 982,
|
||||
CAST_FROM_STRING = 983,
|
||||
CAST_FROM_OBJECT = 984,
|
||||
CAST_FROM_FUNCTION = 985,
|
||||
CAST_FROM_NULL = 989,
|
||||
|
||||
UNHANDLED_EXCEPTION = 999,
|
||||
}
|
||||
/**
|
||||
* Display an error code and stop the program.
|
||||
* @param code an error number to display. eg: 5
|
||||
|
|
|
@ -270,6 +270,7 @@ typedef enum {
|
|||
PANIC_VM_ERROR = 918,
|
||||
PANIC_SETTINGS_CLEARED = 920,
|
||||
PANIC_SETTINGS_OVERLOAD = 921,
|
||||
PANIC_SETTINGS_SECRET_MISSING = 922,
|
||||
|
||||
PANIC_CAST_FIRST = 980,
|
||||
PANIC_CAST_FROM_UNDEFINED = 980,
|
||||
|
|
|
@ -811,6 +811,17 @@ declare const enum DAL {
|
|||
CFG_PIN_BATTSENSE = 75,
|
||||
CFG_PIN_VIBRATION = 76,
|
||||
CFG_PIN_PWREN = 77,
|
||||
CFG_PIN_ROTARY_ENCODER_A = 79,
|
||||
CFG_PIN_ROTARY_ENCODER_B = 80,
|
||||
CFG_PIN_WIFI_MOSI = 82,
|
||||
CFG_PIN_WIFI_MISO = 83,
|
||||
CFG_PIN_WIFI_SCK = 84,
|
||||
CFG_PIN_WIFI_TX = 85,
|
||||
CFG_PIN_WIFI_RX = 86,
|
||||
CFG_PIN_WIFI_CS = 87,
|
||||
CFG_PIN_WIFI_BUSY = 88,
|
||||
CFG_PIN_WIFI_RESET = 89,
|
||||
CFG_PIN_WIFI_GPIO0 = 90,
|
||||
ACCELEROMETER_TYPE_LIS3DH = 50,
|
||||
ACCELEROMETER_TYPE_MMA8453 = 56,
|
||||
ACCELEROMETER_TYPE_FXOS8700 = 60,
|
||||
|
|
|
@ -808,6 +808,17 @@ declare const enum DAL {
|
|||
CFG_PIN_BATTSENSE = 75,
|
||||
CFG_PIN_VIBRATION = 76,
|
||||
CFG_PIN_PWREN = 77,
|
||||
CFG_PIN_ROTARY_ENCODER_A = 79,
|
||||
CFG_PIN_ROTARY_ENCODER_B = 80,
|
||||
CFG_PIN_WIFI_MOSI = 82,
|
||||
CFG_PIN_WIFI_MISO = 83,
|
||||
CFG_PIN_WIFI_SCK = 84,
|
||||
CFG_PIN_WIFI_TX = 85,
|
||||
CFG_PIN_WIFI_RX = 86,
|
||||
CFG_PIN_WIFI_CS = 87,
|
||||
CFG_PIN_WIFI_BUSY = 88,
|
||||
CFG_PIN_WIFI_RESET = 89,
|
||||
CFG_PIN_WIFI_GPIO0 = 90,
|
||||
ACCELEROMETER_TYPE_LIS3DH = 50,
|
||||
ACCELEROMETER_TYPE_MMA8453 = 56,
|
||||
ACCELEROMETER_TYPE_FXOS8700 = 60,
|
||||
|
|
|
@ -811,6 +811,17 @@ declare const enum DAL {
|
|||
CFG_PIN_BATTSENSE = 75,
|
||||
CFG_PIN_VIBRATION = 76,
|
||||
CFG_PIN_PWREN = 77,
|
||||
CFG_PIN_ROTARY_ENCODER_A = 79,
|
||||
CFG_PIN_ROTARY_ENCODER_B = 80,
|
||||
CFG_PIN_WIFI_MOSI = 82,
|
||||
CFG_PIN_WIFI_MISO = 83,
|
||||
CFG_PIN_WIFI_SCK = 84,
|
||||
CFG_PIN_WIFI_TX = 85,
|
||||
CFG_PIN_WIFI_RX = 86,
|
||||
CFG_PIN_WIFI_CS = 87,
|
||||
CFG_PIN_WIFI_BUSY = 88,
|
||||
CFG_PIN_WIFI_RESET = 89,
|
||||
CFG_PIN_WIFI_GPIO0 = 90,
|
||||
ACCELEROMETER_TYPE_LIS3DH = 50,
|
||||
ACCELEROMETER_TYPE_MMA8453 = 56,
|
||||
ACCELEROMETER_TYPE_FXOS8700 = 60,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# ESP32
|
||||
|
||||
## ESP32 SPI
|
||||
|
||||
Ported from Adafruit Circuit Python
|
||||
https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI.
|
||||
|
||||
## Configuration
|
||||
|
||||
* ``PIN_WIFI_CS``, ESP32 CS pin mapping
|
||||
* ``PIN_WIFI_BUSY``, ESP32 CS pin mapping
|
||||
* ``PIN_WIFI_RESET``, ESP32 RESET pin mapping
|
||||
* ``PIN_WIFI_GPIO0`` (optional), ESP32 GPIO0 pin mapping
|
||||
|
||||
The driver uses the default SPI pins. You can override this behavior by specifying these 3 keys.
|
||||
|
||||
* ``PIN_WIFI_MOSI`` (optional), dedicated SPI MOSI pin
|
||||
* ``PIN_WIFI_MISO`` (optional), dedicated SPI MISO pin
|
||||
* ``PIN_WIFI_SCK`` (optional), dedicated SPI SCK pin
|
||||
|
||||
## Access Points and passwords
|
||||
|
||||
The module uses access points and password information stored in the device secrets. These secrets can be set programmatically using ``net.updateAccessPoint`` or via the menu items in Arcade (added via the ``net-game`` extension).
|
||||
|
||||
> *Friendly reminder:* Do not share .uf2 files or programs with secrets!!
|
|
@ -1,18 +1,4 @@
|
|||
namespace esp32spi {
|
||||
export function monotonic(): number {
|
||||
return control.millis() / 1000.0;
|
||||
}
|
||||
|
||||
export function print(msg: string) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
export class AccessPoint {
|
||||
rssi: number;
|
||||
encryption: number;
|
||||
constructor(public ssid: string) { }
|
||||
}
|
||||
|
||||
// pylint: disable=bad-whitespace
|
||||
const _SET_NET_CMD = 0x10
|
||||
const _SET_PASSPHRASE_CMD = 0x11
|
||||
|
@ -79,10 +65,6 @@ namespace esp32spi {
|
|||
export const WL_AP_CONNECTED = 8
|
||||
export const WL_AP_FAILED = 9
|
||||
|
||||
export const TCP_MODE = 0
|
||||
export const UDP_MODE = 1
|
||||
export const TLS_MODE = 2
|
||||
|
||||
|
||||
function buffer1(ch: number) {
|
||||
const b = control.createBuffer(1)
|
||||
|
@ -90,12 +72,10 @@ namespace esp32spi {
|
|||
return b
|
||||
}
|
||||
|
||||
export class SPIController {
|
||||
export class SPIController extends net.Controller {
|
||||
private _socknum_ll: Buffer[];
|
||||
private _locked: boolean;
|
||||
|
||||
static instance: SPIController;
|
||||
|
||||
public wasConnected: boolean;
|
||||
|
||||
constructor(
|
||||
|
@ -106,12 +86,12 @@ namespace esp32spi {
|
|||
private _gpio0: DigitalInOutPin = null,
|
||||
public debug = 0
|
||||
) {
|
||||
super();
|
||||
// if nothing connected, pretend the device is ready -
|
||||
// we'll check for timeout waiting for response instead
|
||||
this._busy.setPull(PinPullMode.PullDown);
|
||||
this._busy.digitalRead();
|
||||
this._socknum_ll = [buffer1(0)]
|
||||
SPIController.instance = this;
|
||||
this._spi.setFrequency(8000000);
|
||||
this.reset();
|
||||
this._locked = false;
|
||||
|
@ -342,14 +322,14 @@ namespace esp32spi {
|
|||
/** The results of the latest SSID scan. Returns a list of dictionaries with
|
||||
'ssid', 'rssi' and 'encryption' entries, one for each AP found
|
||||
*/
|
||||
private getScanNetworks(): AccessPoint[] {
|
||||
private getScanNetworks(): net.AccessPoint[] {
|
||||
let names = this.sendCommandGetResponse(_SCAN_NETWORKS, undefined, undefined)
|
||||
// print("SSID names:", names)
|
||||
// pylint: disable=invalid-name
|
||||
let APs = []
|
||||
let i = 0
|
||||
for (let name of names) {
|
||||
let a_p = new AccessPoint(name.toString())
|
||||
let a_p = new net.AccessPoint(name.toString())
|
||||
let rssi = this.sendCommandGetResponse(_GET_IDX_RSSI_CMD, [buffer1(i)])[0]
|
||||
a_p.rssi = pins.unpackBuffer("<i", rssi)[0]
|
||||
let encr = this.sendCommandGetResponse(_GET_IDX_ENCT_CMD, [buffer1(1)])[0]
|
||||
|
@ -364,7 +344,7 @@ namespace esp32spi {
|
|||
Returns a list of dictionaries with 'ssid', 'rssi' and 'encryption' entries,
|
||||
one for each AP found
|
||||
*/
|
||||
public scanNetworks(): AccessPoint[] {
|
||||
public scanNetworks(): net.AccessPoint[] {
|
||||
this.startScanNetworks()
|
||||
// attempts
|
||||
for (let _ = 0; _ < 10; ++_) {
|
||||
|
@ -372,6 +352,10 @@ namespace esp32spi {
|
|||
// pylint: disable=invalid-name
|
||||
let APs = this.getScanNetworks()
|
||||
if (APs) {
|
||||
if(this.debug) {
|
||||
for(const ap of APs)
|
||||
this.log(0, ` ${ap.ssid} => RSSI ${ap.rssi}`)
|
||||
}
|
||||
return APs
|
||||
}
|
||||
|
||||
|
@ -438,12 +422,16 @@ namespace esp32spi {
|
|||
|
||||
}
|
||||
|
||||
|
||||
get ssid(): Buffer {
|
||||
get ssidBuffer(): Buffer {
|
||||
let resp = this.sendCommandGetResponse(_GET_CURR_SSID_CMD, [hex`ff`])
|
||||
return resp[0]
|
||||
}
|
||||
|
||||
get ssid(): string {
|
||||
const b = this.ssidBuffer;
|
||||
return b ? b.toString() : "";
|
||||
}
|
||||
|
||||
get rssi(): number {
|
||||
let resp = this.sendCommandGetResponse(_GET_CURR_RSSI_CMD, [hex`ff`])
|
||||
return pins.unpackBuffer("<i", resp[0])[0]
|
||||
|
@ -462,17 +450,33 @@ namespace esp32spi {
|
|||
return this.status == WL_CONNECTED
|
||||
}
|
||||
|
||||
/** Connect to an access point using a secrets dictionary
|
||||
that contains a 'ssid' and 'password' entry
|
||||
*/
|
||||
public connect(secrets: any): void {
|
||||
this.connectAP(secrets["ssid"], secrets["password"])
|
||||
get isIdle(): boolean {
|
||||
return this.status == WL_IDLE_STATUS;
|
||||
}
|
||||
|
||||
/** Connect to an access point with given name and password.
|
||||
Will retry up to 10 times and return on success
|
||||
*/
|
||||
public connectAP(ssid: string, password: string): number {
|
||||
/**
|
||||
* Uses RSSID and password in settings to connect to a compatible AP
|
||||
*/
|
||||
public connect(): boolean {
|
||||
if (this.isConnected) return true;
|
||||
|
||||
const wifis = net.knownAccessPoints();
|
||||
const ssids = Object.keys(wifis);
|
||||
const networks = this.scanNetworks()
|
||||
.filter(network => ssids.indexOf(network.ssid) > -1);
|
||||
const network = networks[0];
|
||||
if (network)
|
||||
return this.connectAP(network.ssid, wifis[network.ssid]) == WL_CONNECTED;
|
||||
|
||||
// no compatible SSID
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to an access point with given name and password.
|
||||
* Will retry up to 10 times and return on success
|
||||
*/
|
||||
private connectAP(ssid: string, password: string): number {
|
||||
this.log(0, `Connect to AP ${ssid}`)
|
||||
if (password) {
|
||||
this.wifiSetPassphrase(ssid, password)
|
||||
|
@ -506,7 +510,10 @@ namespace esp32spi {
|
|||
a 4 bytearray
|
||||
*/
|
||||
public hostbyName(hostname: string): Buffer {
|
||||
let resp = this.sendCommandGetResponse(_REQ_HOST_BY_NAME_CMD, [control.createBufferFromUTF8(hostname)])
|
||||
if(!this.connect())
|
||||
return undefined;
|
||||
|
||||
let resp = this.sendCommandGetResponse(_REQ_HOST_BY_NAME_CMD, [control.createBufferFromUTF8(hostname)])
|
||||
if (resp[0][0] != 1) {
|
||||
this.fail("Failed to request hostname")
|
||||
}
|
||||
|
@ -519,8 +526,8 @@ namespace esp32spi {
|
|||
(ttl). Returns a millisecond timing value
|
||||
*/
|
||||
public ping(dest: string, ttl: number = 250): number {
|
||||
if (!this.wasConnected)
|
||||
return -1
|
||||
if(!this.connect())
|
||||
return -1;
|
||||
|
||||
// convert to IP address
|
||||
let ip = this.hostbyName(dest)
|
||||
|
@ -535,6 +542,9 @@ namespace esp32spi {
|
|||
can then be passed to the other socket commands
|
||||
*/
|
||||
public socket(): number {
|
||||
if (!this.connect())
|
||||
this.fail("can't connect");
|
||||
|
||||
this.log(0, "*** Get socket")
|
||||
|
||||
let resp0 = this.sendCommandGetResponse(_GET_SOCKET_CMD)
|
||||
|
@ -545,7 +555,7 @@ namespace esp32spi {
|
|||
|
||||
if (this.debug) {
|
||||
// %d" % resp)
|
||||
print("Allocated socket #" + resp)
|
||||
net.debug("Allocated socket #" + resp)
|
||||
}
|
||||
|
||||
return resp
|
||||
|
@ -556,10 +566,10 @@ namespace esp32spi {
|
|||
'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE
|
||||
(dest must be hostname for TLS_MODE!)
|
||||
*/
|
||||
public socketOpen(socket_num: number, dest: Buffer | string, port: number, conn_mode = TCP_MODE): void {
|
||||
public socketOpen(socket_num: number, dest: Buffer | string, port: number, conn_mode = net.TCP_MODE): void {
|
||||
this._socknum_ll[0][0] = socket_num
|
||||
if (this.debug) {
|
||||
print("*** Open socket: " + dest + ":" + port)
|
||||
net.debug("*** Open socket: " + dest + ":" + port)
|
||||
}
|
||||
|
||||
let port_param = pins.packBuffer(">H", [port])
|
||||
|
@ -598,7 +608,7 @@ namespace esp32spi {
|
|||
/** Write the bytearray buffer to a socket */
|
||||
public socketWrite(socket_num: number, buffer: Buffer): void {
|
||||
if (this.debug > 1) {
|
||||
print("Writing:" + buffer.length)
|
||||
net.debug("Writing:" + buffer.length)
|
||||
}
|
||||
|
||||
this._socknum_ll[0][0] = socket_num
|
||||
|
@ -641,15 +651,15 @@ namespace esp32spi {
|
|||
'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE (dest must
|
||||
be hostname for TLS_MODE!)
|
||||
*/
|
||||
public socketConnect(socket_num: number, dest: string | Buffer, port: number, conn_mode = TCP_MODE): boolean {
|
||||
public socketConnect(socket_num: number, dest: string | Buffer, port: number, conn_mode = net.TCP_MODE): boolean {
|
||||
if (this.debug) {
|
||||
print("*** Socket connect mode " + conn_mode)
|
||||
net.debug("*** Socket connect mode " + conn_mode)
|
||||
}
|
||||
|
||||
this.socketOpen(socket_num, dest, port, conn_mode)
|
||||
let times = monotonic()
|
||||
let times = net.monotonic()
|
||||
// wait 3 seconds
|
||||
while (monotonic() - times < 3) {
|
||||
while (net.monotonic() - times < 3) {
|
||||
if (this.socket_connected(socket_num)) {
|
||||
return true
|
||||
}
|
||||
|
@ -664,7 +674,7 @@ namespace esp32spi {
|
|||
public socketClose(socket_num: number): void {
|
||||
if (this.debug) {
|
||||
// %d" % socket_num)
|
||||
print("*** Closing socket #" + socket_num)
|
||||
net.debug("*** Closing socket #" + socket_num)
|
||||
}
|
||||
|
||||
this._socknum_ll[0][0] = socket_num
|
|
@ -0,0 +1,31 @@
|
|||
namespace esp32spi {
|
||||
let _defaultController: SPIController;
|
||||
function defaultController(): SPIController {
|
||||
if (_defaultController) return _defaultController;
|
||||
|
||||
const cs = pins.pinByCfg(DAL.CFG_PIN_WIFI_CS)
|
||||
const busy = pins.pinByCfg(DAL.CFG_PIN_WIFI_BUSY);
|
||||
const reset = pins.pinByCfg(DAL.CFG_PIN_WIFI_RESET);
|
||||
const gpio0 = pins.pinByCfg(DAL.CFG_PIN_WIFI_GPIO0);
|
||||
if (!cs || !busy || !reset) {
|
||||
control.dmesg(`cs ${!!cs} busy ${!!busy} reset ${!!reset}`)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mosi = pins.pinByCfg(DAL.CFG_PIN_WIFI_MOSI);
|
||||
const miso = pins.pinByCfg(DAL.CFG_PIN_WIFI_MISO);
|
||||
const sck = pins.pinByCfg(DAL.CFG_PIN_WIFI_SCK);
|
||||
let spi: SPI;
|
||||
if (!mosi && !miso && !sck) {
|
||||
spi = pins.spi();
|
||||
} else if (mosi && miso && sck) {
|
||||
spi = pins.createSPI(mosi, miso, sck);
|
||||
}
|
||||
if (!spi)
|
||||
control.panic(control.PXT_PANIC.CODAL_HARDWARE_CONFIGURATION_ERROR);
|
||||
return _defaultController = new SPIController(spi, cs, busy, reset, gpio0);
|
||||
}
|
||||
|
||||
// initialize net
|
||||
new net.Net(defaultController);
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
{
|
||||
"name": "esp32spi",
|
||||
"name": "esp32",
|
||||
"description": "ESP32 over SPI - beta",
|
||||
"files": [
|
||||
"esp32spi.ts",
|
||||
"socket.ts",
|
||||
"requests.ts",
|
||||
"wifimanager.ts",
|
||||
"net.ts",
|
||||
"README.md"
|
||||
],
|
||||
|
@ -15,7 +12,7 @@
|
|||
"public": true,
|
||||
"dependencies": {
|
||||
"core": "file:../core",
|
||||
"net": "file:../net"
|
||||
"net": "file:../net",
|
||||
"settings": "file:../settings"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
function test() {
|
||||
|
||||
const log = console.log;
|
||||
const esp = new esp32spi.SPIController(pins.spi(),
|
||||
pins.D13, pins.D11, pins.D12, pins.D10, 1)
|
||||
|
||||
if (!esp.isIdle)
|
||||
return
|
||||
|
||||
log(`Firmware vers. ${esp.firmwareVersion}`)
|
||||
log(`MAC addr: ${esp.MACaddress.toHex()}`)
|
||||
log("Temp: " + esp.getTemperature())
|
||||
|
||||
if (!esp.connect()) {
|
||||
log("can't connect")
|
||||
return
|
||||
}
|
||||
|
||||
log("ping: " + esp.ping("bing.com"))
|
||||
}
|
||||
|
||||
test();
|
|
@ -1,6 +0,0 @@
|
|||
# WiFi
|
||||
|
||||
## ESP32 SPI
|
||||
|
||||
Ported from Adafruit Circuit Python
|
||||
https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI.
|
|
@ -1,23 +0,0 @@
|
|||
namespace esp32spi {
|
||||
export class Net extends net.Net {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
createSocket(host: string, port: number): net.Socket {
|
||||
const socket = new Socket(host, port);
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
|
||||
export class NetTLS extends net.Net {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
createSocket(host: string, port: number): net.Socket {
|
||||
const socket = new Socket(host, port, TLS_MODE);
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
namespace esp32spi {
|
||||
export class Response {
|
||||
socket: Socket
|
||||
_cached: Buffer
|
||||
status_code: number
|
||||
reason: string
|
||||
_read_so_far: number
|
||||
headers: StringMap
|
||||
|
||||
/** The response from a request, contains all the headers/content */
|
||||
constructor(sock: Socket) {
|
||||
this.socket = sock
|
||||
this._cached = null
|
||||
this.status_code = null
|
||||
this.reason = null
|
||||
this._read_so_far = 0
|
||||
this.headers = {}
|
||||
}
|
||||
|
||||
public close() {
|
||||
/** Close, delete and collect the response data */
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
this._cached = null
|
||||
}
|
||||
|
||||
get content() {
|
||||
/** The HTTP content direct from the socket, as bytes */
|
||||
let content_length = parseInt(this.headers["content-length"]) || 0
|
||||
|
||||
// print("Content length:", content_length)
|
||||
if (this._cached === null) {
|
||||
this._cached = this.socket.read(content_length)
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
|
||||
// print("Buffer length:", len(self._cached))
|
||||
return this._cached
|
||||
}
|
||||
|
||||
/** The HTTP content, encoded into a string according to the HTTP
|
||||
header encoding
|
||||
*/
|
||||
get text() {
|
||||
return this.content.toString()
|
||||
}
|
||||
|
||||
get json() {
|
||||
return JSON.parse(this.text)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `HTTP ${this.status_code}; ${Object.keys(this.headers).length} headers; ${this._cached ? this._cached.length : -1} bytes content`
|
||||
}
|
||||
}
|
||||
|
||||
export type StringMap = { [v: string]: string; };
|
||||
|
||||
export interface RequestOptions {
|
||||
data?: string | Buffer;
|
||||
json?: any; // will call JSON.stringify()
|
||||
headers?: StringMap;
|
||||
stream?: boolean;
|
||||
timeout?: number; // in ms
|
||||
}
|
||||
|
||||
export function dataAsBuffer(data: string | Buffer): Buffer {
|
||||
if (data == null)
|
||||
return null
|
||||
if (typeof data == "string")
|
||||
return control.createBufferFromUTF8(data)
|
||||
return data
|
||||
}
|
||||
|
||||
// TODO move to PXT
|
||||
// also note this doesn't handle unicode, but neither does JS (there's toLocaleLowerCase())
|
||||
export function toLowerCase(s: string) {
|
||||
let r = ""
|
||||
let prev = 0
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c = s.charCodeAt(i)
|
||||
if (65 <= c && c <= 90) {
|
||||
r += s.slice(prev, i) + String.fromCharCode(c + 32)
|
||||
prev = i + 1
|
||||
}
|
||||
}
|
||||
r += s.slice(prev)
|
||||
return r
|
||||
}
|
||||
|
||||
/*
|
||||
>>> "a,b,c,d,e".split(",", 2)
|
||||
['a', 'b', 'c,d,e']
|
||||
*/
|
||||
function pysplit(str: string, sep:string, limit: number) {
|
||||
const arr = str.split(sep)
|
||||
if (arr.length >= limit) {
|
||||
return arr.slice(0, limit).concat([arr.slice(limit).join(sep)])
|
||||
} else {
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Perform an HTTP request to the given url which we will parse to determine
|
||||
whether to use SSL ('https://') or not. We can also send some provided 'data'
|
||||
or a json dictionary which we will stringify. 'headers' is optional HTTP headers
|
||||
sent along. 'stream' will determine if we buffer everything, or whether to only
|
||||
read only when requested
|
||||
|
||||
*/
|
||||
export function request(method: string, url: string, options?: RequestOptions): Response {
|
||||
if (!options) options = {};
|
||||
if (!options.headers) {
|
||||
options.headers = {}
|
||||
}
|
||||
|
||||
const tmp = pysplit(url, "/", 3)
|
||||
let proto = tmp[0]
|
||||
let host = tmp[2]
|
||||
let path = tmp[3] || ""
|
||||
// replace spaces in path
|
||||
// TODO
|
||||
// path = path.replace(" ", "%20")
|
||||
|
||||
let port = 0
|
||||
if (proto == "http:") {
|
||||
port = 80
|
||||
} else if (proto == "https:") {
|
||||
port = 443
|
||||
} else {
|
||||
control.fail("Unsupported protocol: " + proto)
|
||||
}
|
||||
|
||||
if (host.indexOf(":") >= 0) {
|
||||
const tmp = host.split(":")
|
||||
host = tmp[0]
|
||||
port = parseInt(tmp[1])
|
||||
}
|
||||
|
||||
let ipaddr = esp32spi.SPIController.instance.hostbyName(host)
|
||||
|
||||
let sock: Socket;
|
||||
let conntype = esp32spi.TCP_MODE
|
||||
if (proto == "https:") {
|
||||
conntype = esp32spi.TLS_MODE
|
||||
// for SSL we need to know the host name
|
||||
sock = new Socket(host, port, conntype)
|
||||
} else {
|
||||
sock = new Socket(ipaddr, port, conntype)
|
||||
}
|
||||
// our response
|
||||
let resp = new Response(sock)
|
||||
// socket read timeout
|
||||
sock.setTimeout(options.timeout)
|
||||
|
||||
sock.connect();
|
||||
sock.send(`${method} /${path} HTTP/1.0\r\n`)
|
||||
|
||||
if (!options.headers["Host"])
|
||||
sock.send(`Host: ${host}\r\n`)
|
||||
|
||||
if (!options.headers["User-Agent"])
|
||||
sock.send("User-Agent: MakeCode ESP32\r\n")
|
||||
|
||||
// Iterate over keys to avoid tuple alloc
|
||||
for (let k of Object.keys(options.headers))
|
||||
sock.send(`${k}: ${options.headers[k]}\r\n`)
|
||||
|
||||
if (options.json != null) {
|
||||
control.assert(options.data == null, 100)
|
||||
options.data = JSON.stringify(options.json)
|
||||
sock.send("Content-Type: application/json\r\n")
|
||||
}
|
||||
|
||||
let dataBuf = dataAsBuffer(options.data)
|
||||
|
||||
if (dataBuf)
|
||||
sock.send(`Content-Length: ${dataBuf.length}\r\n`)
|
||||
|
||||
sock.send("\r\n")
|
||||
if (dataBuf)
|
||||
sock.send(dataBuf)
|
||||
|
||||
let line = sock.readLine()
|
||||
// print(line)
|
||||
let line2 = pysplit(line, " ", 2)
|
||||
let status = parseInt(line2[1])
|
||||
let reason = ""
|
||||
if (line2.length > 2) {
|
||||
reason = line2[2]
|
||||
}
|
||||
|
||||
while (true) {
|
||||
line = sock.readLine()
|
||||
if (!line || line == "\r\n") {
|
||||
break
|
||||
}
|
||||
|
||||
// print("**line: ", line)
|
||||
const tmp = pysplit(line, ": ", 1)
|
||||
let title = tmp[0]
|
||||
let content = tmp[1]
|
||||
if (title && content) {
|
||||
resp.headers[toLowerCase(title)] = toLowerCase(content)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
elif line.startswith(b"Location:") and not 200 <= status <= 299:
|
||||
raise NotImplementedError("Redirects not yet supported")
|
||||
*/
|
||||
|
||||
if ((resp.headers["transfer-encoding"] || "").indexOf("chunked") >= 0)
|
||||
control.fail("not supported chunked encoding")
|
||||
|
||||
resp.status_code = status
|
||||
resp.reason = reason
|
||||
return resp
|
||||
}
|
||||
|
||||
/** Send HTTP HEAD request */
|
||||
export function head(url: string, options?: RequestOptions) {
|
||||
return request("HEAD", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP GET request */
|
||||
export function get(url: string, options?: RequestOptions) {
|
||||
return request("GET", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP POST request */
|
||||
export function post(url: string, options?: RequestOptions) {
|
||||
return request("POST", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP PATCH request */
|
||||
export function patch(url: string, options?: RequestOptions) {
|
||||
return request("PATCH", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP PUT request */
|
||||
export function put(url: string, options?: RequestOptions) {
|
||||
return request("PUT", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP DELETE request */
|
||||
export function del(url: string, options?: RequestOptions) {
|
||||
return request("DELETE", url, options)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
export interface Secrets {
|
||||
connString: string;
|
||||
wifi: pxt.StringMap;
|
||||
}
|
||||
// this is to be overridden in a separate file
|
||||
export let secrets: Secrets;
|
||||
function test() {
|
||||
|
||||
const log = console.log;
|
||||
const esp = new esp32spi.SPIController(pins.spi(),
|
||||
pins.D13, pins.D11, pins.D12, pins.D10, 1)
|
||||
|
||||
if (esp.status != esp32spi.WL_IDLE_STATUS)
|
||||
return
|
||||
|
||||
log(`Firmware vers. ${esp.firmwareVersion}`)
|
||||
log(`MAC addr: ${esp.MACaddress.toHex()}`)
|
||||
log("Temp: " + esp.getTemperature())
|
||||
|
||||
const networks = esp.scanNetworks()
|
||||
|
||||
log(JSON.stringify(secrets.wifi))
|
||||
|
||||
for (const ap of networks)
|
||||
log(`\t${ap.ssid}\t\tRSSI: ${ap.rssi}`)
|
||||
|
||||
for (let k of Object.keys(secrets.wifi)) {
|
||||
if (networks.some(n => n.ssid == k)) {
|
||||
log("connecting to " + k)
|
||||
esp.connectAP(k, secrets.wifi[k])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!esp.isConnected) {
|
||||
log("can't connect")
|
||||
return
|
||||
}
|
||||
|
||||
log("ping: " + esp.ping("bing.com"))
|
||||
}
|
||||
|
||||
test();
|
|
@ -308,7 +308,7 @@ namespace scene.systemMenu {
|
|||
power.deepSleep();
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
export function closeMenu() {
|
||||
if (instance) {
|
||||
instance.dispose();
|
||||
instance = undefined;
|
||||
|
|
|
@ -320,7 +320,7 @@ namespace mqtt {
|
|||
|
||||
private mqttHandlers: MQTTHandler[];
|
||||
|
||||
constructor(opt: IConnectionOptions, net: net.Net) {
|
||||
constructor(opt: IConnectionOptions) {
|
||||
super();
|
||||
|
||||
this.wdId = Constants.Uninitialized;
|
||||
|
@ -336,7 +336,7 @@ namespace mqtt {
|
|||
}
|
||||
|
||||
this.opt = opt;
|
||||
this.net = net;
|
||||
this.net = net.Net.instance;
|
||||
}
|
||||
|
||||
private static describe(code: ConnectReturnCode): string {
|
||||
|
@ -396,7 +396,7 @@ namespace mqtt {
|
|||
}, Constants.WatchDogInterval * 1000);
|
||||
}
|
||||
|
||||
this.sct = this.net.createSocket(this.opt.host, this.opt.port);
|
||||
this.sct = this.net.createSocket(this.opt.host, this.opt.port, true);
|
||||
this.sct.onOpen(() => {
|
||||
this.log('Network connection established.');
|
||||
this.emit('connect');
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "net-game",
|
||||
"description": "WiFi support in Arcade - beta",
|
||||
"files": [
|
||||
"settings.ts"
|
||||
],
|
||||
"public": true,
|
||||
"dependencies": {
|
||||
"core": "file:../core",
|
||||
"settings": "file:../settings",
|
||||
"game": "file:../game"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
namespace net {
|
||||
class Configurator {
|
||||
private accessPoints: net.AccessPoint[];
|
||||
private apIndex: number;
|
||||
private scanning: boolean;
|
||||
private wifi: net.Controller;
|
||||
|
||||
constructor() {
|
||||
this.scanning = false;
|
||||
this.apIndex = 0;
|
||||
}
|
||||
|
||||
private select() {
|
||||
const ap = this.accessPoints && this.accessPoints[this.apIndex];
|
||||
if (ap) {
|
||||
const wifis = net.knownAccessPoints();
|
||||
const known = wifis[ap.ssid] !== undefined ? "*" : "?";
|
||||
console.log(`${known} ${ap.ssid}`);
|
||||
}
|
||||
}
|
||||
|
||||
private connect() {
|
||||
console.log("connecting...")
|
||||
this.wifi.connect();
|
||||
console.log(this.wifi.isConnected ? `connected to ${this.wifi.ssid}` : `disconnected`);
|
||||
if (this.wifi.isConnected) {
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
const ping = this.wifi.ping("bing.com")
|
||||
console.log(`bing.com ping ${ping}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scan() {
|
||||
if (this.scanning) return;
|
||||
|
||||
this.scanning = true;
|
||||
const mac = this.wifi.MACaddress;
|
||||
console.log(`MAC: ${mac ? mac.toHex() : "???"}`)
|
||||
console.log("scanning...")
|
||||
control.runInBackground(() => {
|
||||
this.accessPoints = this.wifi.scanNetworks()
|
||||
if (this.accessPoints && this.accessPoints.length) {
|
||||
const wifis = net.knownAccessPoints();
|
||||
for (let i = 0; i < this.accessPoints.length; ++i) {
|
||||
const ap = this.accessPoints[i];
|
||||
const known = wifis[ap.ssid] !== undefined ? "*" : "?";
|
||||
console.log(` ${known} ${ap.ssid}`);
|
||||
}
|
||||
console.log(" ");
|
||||
this.apIndex = 0;
|
||||
console.log("*: AP known")
|
||||
console.log("up/down: select AP")
|
||||
console.log("left: erase AP info")
|
||||
console.log("right: enter AP password")
|
||||
console.log("A: connect")
|
||||
console.log(" ");
|
||||
this.select();
|
||||
}
|
||||
this.scanning = false;
|
||||
});
|
||||
}
|
||||
|
||||
main() {
|
||||
this.wifi = net.Net.instance.controller;
|
||||
if (!this.wifi) {
|
||||
console.log("WiFi module not configured");
|
||||
return;
|
||||
}
|
||||
pauseUntil(() => this.wifi.isIdle, 30000);
|
||||
if (!this.wifi.isIdle) {
|
||||
console.log("WiFi module not responding")
|
||||
return;
|
||||
}
|
||||
controller.up.onEvent(ControllerButtonEvent.Pressed, () => {
|
||||
this.apIndex = this.apIndex + 1;
|
||||
if (this.accessPoints)
|
||||
this.apIndex = this.apIndex % this.accessPoints.length;
|
||||
this.select();
|
||||
})
|
||||
controller.down.onEvent(ControllerButtonEvent.Pressed, () => {
|
||||
this.apIndex = this.apIndex - 1;
|
||||
this.apIndex = (this.apIndex + this.accessPoints.length) % this.accessPoints.length;
|
||||
this.select();
|
||||
})
|
||||
controller.left.onEvent(ControllerButtonEvent.Pressed, () => {
|
||||
const ap = this.accessPoints && this.accessPoints[this.apIndex];
|
||||
if (!ap) return;
|
||||
net.updateAccessPoint(ap.ssid, undefined);
|
||||
console.log(`password erased`)
|
||||
this.scan();
|
||||
})
|
||||
controller.right.onEvent(ControllerButtonEvent.Pressed, () => {
|
||||
const ap = this.accessPoints && this.accessPoints[this.apIndex];
|
||||
if (!ap) return;
|
||||
game.consoleOverlay.setVisible(false);
|
||||
const pwd = game.askForString(`password for ${ap.ssid}`, 24);
|
||||
game.consoleOverlay.setVisible(true);
|
||||
net.updateAccessPoint(ap.ssid, pwd);
|
||||
console.log(`password saved`)
|
||||
this.scan();
|
||||
})
|
||||
controller.A.onEvent(ControllerButtonEvent.Pressed, () => {
|
||||
this.connect();
|
||||
})
|
||||
controller.B.onEvent(ControllerButtonEvent.Pressed, () => {
|
||||
game.popScene();
|
||||
game.consoleOverlay.setVisible(false);
|
||||
});
|
||||
this.scan();
|
||||
}
|
||||
}
|
||||
|
||||
function wifiSystemMenu() {
|
||||
scene.systemMenu.closeMenu();
|
||||
game.pushScene();
|
||||
game.consoleOverlay.setVisible(true);
|
||||
console.log("WiFi configuration")
|
||||
const config = new Configurator();
|
||||
config.main()
|
||||
}
|
||||
|
||||
scene.systemMenu.addEntry(
|
||||
() => "WiFi",
|
||||
() => wifiSystemMenu(),
|
||||
img`
|
||||
. . . . . . . . . . . . . . . .
|
||||
. . . . . . 8 8 8 8 . . . . . .
|
||||
. . . . 8 8 8 6 6 6 8 8 . . . .
|
||||
. . . 8 6 6 6 6 6 6 6 6 8 . . .
|
||||
. . 8 6 6 . . . . . . 6 6 8 . .
|
||||
. 8 6 6 . . . . . . . . 6 6 8 .
|
||||
8 6 6 . . . 8 8 8 8 . . . 6 6 8
|
||||
. 6 . . . 8 6 6 6 6 8 . . . 6 .
|
||||
. . . . 8 6 6 6 6 6 6 8 . . . .
|
||||
. . . 8 6 6 . . . . 6 6 8 . . .
|
||||
. . . . 6 . . . . . . 6 . . . .
|
||||
. . . . . . . 8 8 . . . . . . .
|
||||
. . . . . . 8 6 6 8 . . . . . .
|
||||
. . . . . . 6 6 6 6 . . . . . .
|
||||
. . . . . . . 6 6 . . . . . . .
|
||||
. . . . . . . . . . . . . . . .
|
||||
`);
|
||||
}
|
|
@ -1,3 +1,22 @@
|
|||
# Net
|
||||
|
||||
Networking abstractions
|
||||
Networking abstractions and drivers
|
||||
|
||||
## WiFi module configuration
|
||||
|
||||
* ``PIN_WIFI_CS``, ESP32 CS pin mapping
|
||||
* ``PIN_WIFI_BUSY``, ESP32 CS pin mapping
|
||||
* ``PIN_WIFI_RESET``, ESP32 RESET pin mapping
|
||||
* ``PIN_WIFI_GPIO0`` (optional), ESP32 GPIO0 pin mapping
|
||||
|
||||
The driver uses the default SPI pins. You can override this behavior by specifying these 3 keys.
|
||||
|
||||
* ``PIN_WIFI_MOSI`` (optional), dedicated SPI MOSI pin
|
||||
* ``PIN_WIFI_MISO`` (optional), dedicated SPI MISO pin
|
||||
* ``PIN_WIFI_SCK`` (optional), dedicated SPI SCK pin
|
||||
|
||||
## Access Points and passwords
|
||||
|
||||
The module uses access points and password information stored in the device secrets. These secrets can be set programmatically using ``net.updateAccessPoint`` or via the menu items in Arcade (added via the ``net-game`` extension).
|
||||
|
||||
> *Friendly reminder:* Do not share .uf2 files or programs with secrets!!
|
|
@ -0,0 +1,41 @@
|
|||
namespace net {
|
||||
export class Controller {
|
||||
constructor() { }
|
||||
|
||||
public scanNetworks(): net.AccessPoint[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public socket(): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public socketConnect(socket_num: number, dest: string | Buffer, port: number, conn_mode = TCP_MODE): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public socketWrite(socket_num: number, buffer: Buffer): void {
|
||||
}
|
||||
|
||||
public socketAvailable(socket_num: number): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public socketRead(socket_num: number, size: number): Buffer {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public socketClose(socket_num: number): void {
|
||||
}
|
||||
|
||||
public hostbyName(hostname: string): Buffer {
|
||||
return undefined;
|
||||
}
|
||||
get isIdle(): boolean { return false; }
|
||||
get isConnected(): boolean { return false; }
|
||||
connect(): void { }
|
||||
get ssid(): string { return undefined; }
|
||||
get MACaddress(): Buffer { return undefined; }
|
||||
public ping(dest: string, ttl: number = 250): number { return -1; }
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
namespace esp32spi {
|
||||
namespace net {
|
||||
|
||||
export const SOCK_STREAM = 1
|
||||
export const AF_INET = 2
|
||||
export const MAX_PACKET = 4000
|
||||
export const TCP_MODE = 0
|
||||
export const UDP_MODE = 1
|
||||
export const TLS_MODE = 2
|
||||
|
||||
export class Socket implements net.Socket {
|
||||
export class ControllerSocket implements net.Socket {
|
||||
_buffer: Buffer;
|
||||
_socknum: number;
|
||||
_timeout: number;
|
||||
|
@ -17,12 +20,12 @@ namespace esp32spi {
|
|||
/** A simplified implementation of the Python 'socket' class, for connecting
|
||||
through an interface to a remote device
|
||||
*/
|
||||
constructor(private host: string | Buffer, private port: number, private conntype: number = null) {
|
||||
constructor(private controller: Controller, private host: string | Buffer, private port: number, private conntype: number = null) {
|
||||
if (this.conntype === null) {
|
||||
this.conntype = esp32spi.TCP_MODE
|
||||
this.conntype = net.TCP_MODE
|
||||
}
|
||||
this._buffer = hex``
|
||||
this._socknum = esp32spi.SPIController.instance.socket()
|
||||
this._socknum = this.controller.socket()
|
||||
this.setTimeout(0)
|
||||
}
|
||||
|
||||
|
@ -31,7 +34,7 @@ namespace esp32spi {
|
|||
depending on the underlying interface
|
||||
*/
|
||||
public connect() {
|
||||
if (!esp32spi.SPIController.instance.socketConnect(this._socknum, this.host, this.port, this.conntype)) {
|
||||
if (!this.controller.socketConnect(this._socknum, this.host, this.port, this.conntype)) {
|
||||
this.error(`failed to connect to ${this.host}`)
|
||||
return;
|
||||
}
|
||||
|
@ -45,7 +48,7 @@ namespace esp32spi {
|
|||
/** Send some data to the socket */
|
||||
public send(data: string | Buffer) {
|
||||
//console.log("sock wr: " + data)
|
||||
esp32spi.SPIController.instance.socketWrite(this._socknum, dataAsBuffer(data))
|
||||
this.controller.socketWrite(this._socknum, net.dataAsBuffer(data))
|
||||
}
|
||||
|
||||
private error(msg: string) {
|
||||
|
@ -85,9 +88,9 @@ namespace esp32spi {
|
|||
let stamp = monotonic()
|
||||
while (this._buffer.indexOf(hex`0d0a`) < 0) {
|
||||
// there's no line already in there, read some more
|
||||
let avail = Math.min(esp32spi.SPIController.instance.socketAvailable(this._socknum), MAX_PACKET)
|
||||
let avail = Math.min(this.controller.socketAvailable(this._socknum), MAX_PACKET)
|
||||
if (avail) {
|
||||
this._buffer = this._buffer.concat(esp32spi.SPIController.instance.socketRead(this._socknum, avail))
|
||||
this._buffer = this._buffer.concat(this.controller.socketRead(this._socknum, avail))
|
||||
} else if (this._timeout > 0 && monotonic() - stamp > this._timeout) {
|
||||
// Make sure to close socket so that we don't exhaust sockets.
|
||||
this.close()
|
||||
|
@ -107,9 +110,9 @@ namespace esp32spi {
|
|||
// print("Socket read", size)
|
||||
if (size == 0) {
|
||||
if (this._buffer.length == 0) {
|
||||
let avail = Math.min(esp32spi.SPIController.instance.socketAvailable(this._socknum), MAX_PACKET)
|
||||
let avail = Math.min(this.controller.socketAvailable(this._socknum), MAX_PACKET)
|
||||
if (avail)
|
||||
this._buffer = this._buffer.concat(esp32spi.SPIController.instance.socketRead(this._socknum, avail))
|
||||
this._buffer = this._buffer.concat(this.controller.socketRead(this._socknum, avail))
|
||||
}
|
||||
let ret = this._buffer
|
||||
this._buffer = hex``
|
||||
|
@ -121,10 +124,10 @@ namespace esp32spi {
|
|||
let received = []
|
||||
while (to_read > 0) {
|
||||
// print("Bytes to read:", to_read)
|
||||
let avail = Math.min(esp32spi.SPIController.instance.socketAvailable(this._socknum), MAX_PACKET)
|
||||
let avail = Math.min(this.controller.socketAvailable(this._socknum), MAX_PACKET)
|
||||
if (avail) {
|
||||
stamp = monotonic()
|
||||
let recv = esp32spi.SPIController.instance.socketRead(this._socknum, Math.min(to_read, avail))
|
||||
let recv = this.controller.socketRead(this._socknum, Math.min(to_read, avail))
|
||||
received.push(recv)
|
||||
to_read -= recv.length
|
||||
}
|
||||
|
@ -157,7 +160,7 @@ namespace esp32spi {
|
|||
/** Close the socket, after reading whatever remains */
|
||||
public close() {
|
||||
this._closed = true;
|
||||
esp32spi.SPIController.instance.socketClose(this._socknum)
|
||||
this.controller.socketClose(this._socknum)
|
||||
if (this._closeHandler)
|
||||
this._closeHandler();
|
||||
}
|
|
@ -1,7 +1,20 @@
|
|||
namespace net {
|
||||
export let logPriority = ConsolePriority.Silent;
|
||||
export function log(msg: string) {
|
||||
console.add(logPriority, `net: ` + msg);
|
||||
console.add(logPriority, "net:" + msg);
|
||||
}
|
||||
export function debug(msg: string) {
|
||||
console.add(ConsolePriority.Debug, "net:" + msg);
|
||||
}
|
||||
|
||||
export function monotonic(): number {
|
||||
return control.millis() / 1000.0;
|
||||
}
|
||||
|
||||
export class AccessPoint {
|
||||
rssi: number;
|
||||
encryption: number;
|
||||
constructor(public ssid: string) { }
|
||||
}
|
||||
|
||||
export interface Socket {
|
||||
|
@ -13,12 +26,51 @@ namespace net {
|
|||
onClose(handler: () => void): void;
|
||||
onError(handler: (msg: string) => void): void;
|
||||
onMessage(handler: (data: Buffer) => void): void;
|
||||
setTimeout(millis: number): void;
|
||||
readLine(): string;
|
||||
}
|
||||
|
||||
export class Net {
|
||||
constructor() {}
|
||||
createSocket(host: string, port: number): Socket {
|
||||
private _controller: Controller;
|
||||
constructor(private factory: () => Controller) {
|
||||
Net.instance = this;
|
||||
}
|
||||
|
||||
static instance: Net;
|
||||
|
||||
get controller(): net.Controller {
|
||||
if (!this._controller)
|
||||
this._controller = this.factory();
|
||||
return this._controller;
|
||||
}
|
||||
|
||||
createSocket(host: string, port: number, secure: boolean): net.Socket {
|
||||
const c = this.controller;
|
||||
if (!c) return undefined;
|
||||
const socket = new net.ControllerSocket(c, host, port, secure ? net.TLS_MODE : net.TCP_MODE);
|
||||
return socket;
|
||||
}
|
||||
|
||||
hostByName(host: string): string {
|
||||
const c= this.controller;
|
||||
if (!c) return undefined;
|
||||
const b = this.controller.hostbyName(host);
|
||||
if (b) return b.toString();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const AP_SECRETS_KEY = "wifi";
|
||||
/**
|
||||
* Gets the map of SSID -> password pairs
|
||||
*/
|
||||
export function knownAccessPoints(): StringMap {
|
||||
return settings.deviceSecrets.readSecret(AP_SECRETS_KEY) || {};
|
||||
}
|
||||
|
||||
export function updateAccessPoint(ssid: string, password: string) {
|
||||
const k: StringMap = {};
|
||||
k[ssid] = password;
|
||||
settings.deviceSecrets.updateSecret(AP_SECRETS_KEY, k);
|
||||
}
|
||||
}
|
|
@ -3,11 +3,14 @@
|
|||
"description": "Networking abstractions",
|
||||
"files": [
|
||||
"README.md",
|
||||
"controller.ts",
|
||||
"controllersocket.ts",
|
||||
"net.ts",
|
||||
"requests.ts"
|
||||
],
|
||||
"public": true,
|
||||
"dependencies": {
|
||||
"core": "file:../core"
|
||||
"core": "file:../core",
|
||||
"settings": "file:../settings"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ namespace net {
|
|||
_read_so_far: number
|
||||
headers: StringMap;
|
||||
|
||||
/** The response from a request, contains all the headers/content */
|
||||
/**
|
||||
* The response from a request, contains all the headers/content
|
||||
*/
|
||||
constructor(private socket: Socket) {
|
||||
this._cached = null
|
||||
this.status_code = null
|
||||
|
@ -15,7 +17,9 @@ namespace net {
|
|||
this.headers = {}
|
||||
}
|
||||
|
||||
/** Close, delete and collect the response data */
|
||||
/**
|
||||
* Close, delete and collect the response data
|
||||
*/
|
||||
public close() {
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
|
@ -24,7 +28,9 @@ namespace net {
|
|||
this._cached = null
|
||||
}
|
||||
|
||||
/** The HTTP content direct from the socket, as bytes */
|
||||
/**
|
||||
* The HTTP content direct from the socket, as bytes
|
||||
*/
|
||||
get content() {
|
||||
let content_length = parseInt(this.headers["content-length"]) || 0
|
||||
|
||||
|
@ -46,9 +52,13 @@ namespace net {
|
|||
return this.content.toString()
|
||||
}
|
||||
|
||||
public json() {
|
||||
get json() {
|
||||
return JSON.parse(this.text)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `HTTP ${this.status_code}; ${Object.keys(this.headers).length} headers; ${this._cached ? this._cached.length : -1} bytes content`
|
||||
}
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
|
@ -66,4 +76,164 @@ namespace net {
|
|||
return control.createBufferFromUTF8(data)
|
||||
return data
|
||||
}
|
||||
|
||||
/*
|
||||
>>> "a,b,c,d,e".split(",", 2)
|
||||
['a', 'b', 'c,d,e']
|
||||
*/
|
||||
function pysplit(str: string, sep: string, limit: number) {
|
||||
const arr = str.split(sep)
|
||||
if (arr.length >= limit) {
|
||||
return arr.slice(0, limit).concat([arr.slice(limit).join(sep)])
|
||||
} else {
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Perform an HTTP request to the given url which we will parse to determine
|
||||
whether to use SSL ('https://') or not. We can also send some provided 'data'
|
||||
or a json dictionary which we will stringify. 'headers' is optional HTTP headers
|
||||
sent along. 'stream' will determine if we buffer everything, or whether to only
|
||||
read only when requested
|
||||
|
||||
*/
|
||||
export function request(method: string, url: string, options?: RequestOptions): net.Response {
|
||||
if (!options) options = {};
|
||||
if (!options.headers) {
|
||||
options.headers = {}
|
||||
}
|
||||
|
||||
const tmp = pysplit(url, "/", 3)
|
||||
let proto = tmp[0]
|
||||
let host = tmp[2]
|
||||
let path = tmp[3] || ""
|
||||
// replace spaces in path
|
||||
// TODO
|
||||
// path = path.replace(" ", "%20")
|
||||
|
||||
let port = 0
|
||||
if (proto == "http:") {
|
||||
port = 80
|
||||
} else if (proto == "https:") {
|
||||
port = 443
|
||||
} else {
|
||||
control.fail("Unsupported protocol: " + proto)
|
||||
}
|
||||
|
||||
if (host.indexOf(":") >= 0) {
|
||||
const tmp = host.split(":")
|
||||
host = tmp[0]
|
||||
port = parseInt(tmp[1])
|
||||
}
|
||||
|
||||
let ipaddr = net.Net.instance.hostByName(host)
|
||||
|
||||
let sock: Socket;
|
||||
if (proto == "https:") {
|
||||
// for SSL we need to know the host name
|
||||
sock = net.Net.instance.createSocket(host, port, true)
|
||||
} else {
|
||||
sock = net.Net.instance.createSocket(ipaddr, port, false)
|
||||
}
|
||||
// our response
|
||||
let resp = new Response(sock)
|
||||
// socket read timeout
|
||||
sock.setTimeout(options.timeout)
|
||||
|
||||
sock.connect();
|
||||
sock.send(`${method} /${path} HTTP/1.0\r\n`)
|
||||
|
||||
if (!options.headers["Host"])
|
||||
sock.send(`Host: ${host}\r\n`)
|
||||
|
||||
if (!options.headers["User-Agent"])
|
||||
sock.send("User-Agent: MakeCode ESP32\r\n")
|
||||
|
||||
// Iterate over keys to avoid tuple alloc
|
||||
for (let k of Object.keys(options.headers))
|
||||
sock.send(`${k}: ${options.headers[k]}\r\n`)
|
||||
|
||||
if (options.json != null) {
|
||||
control.assert(options.data == null, 100)
|
||||
options.data = JSON.stringify(options.json)
|
||||
sock.send("Content-Type: application/json\r\n")
|
||||
}
|
||||
|
||||
let dataBuf = dataAsBuffer(options.data)
|
||||
|
||||
if (dataBuf)
|
||||
sock.send(`Content-Length: ${dataBuf.length}\r\n`)
|
||||
|
||||
sock.send("\r\n")
|
||||
if (dataBuf)
|
||||
sock.send(dataBuf)
|
||||
|
||||
let line = sock.readLine()
|
||||
// print(line)
|
||||
let line2 = pysplit(line, " ", 2)
|
||||
let status = parseInt(line2[1])
|
||||
let reason = ""
|
||||
if (line2.length > 2) {
|
||||
reason = line2[2]
|
||||
}
|
||||
|
||||
while (true) {
|
||||
line = sock.readLine()
|
||||
if (!line || line == "\r\n") {
|
||||
break
|
||||
}
|
||||
|
||||
// print("**line: ", line)
|
||||
const tmp = pysplit(line, ": ", 1)
|
||||
let title = tmp[0]
|
||||
let content = tmp[1]
|
||||
if (title && content) {
|
||||
resp.headers[title.toLowerCase()] = content.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
elif line.startswith(b"Location:") and not 200 <= status <= 299:
|
||||
raise NotImplementedError("Redirects not yet supported")
|
||||
*/
|
||||
|
||||
if ((resp.headers["transfer-encoding"] || "").indexOf("chunked") >= 0)
|
||||
control.fail("not supported chunked encoding")
|
||||
|
||||
resp.status_code = status
|
||||
resp.reason = reason
|
||||
return resp
|
||||
}
|
||||
|
||||
/** Send HTTP HEAD request */
|
||||
export function head(url: string, options?: RequestOptions) {
|
||||
return request("HEAD", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP GET request */
|
||||
export function get(url: string, options?: RequestOptions) {
|
||||
return request("GET", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP POST request */
|
||||
export function post(url: string, options?: RequestOptions) {
|
||||
return request("POST", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP PATCH request */
|
||||
export function patch(url: string, options?: RequestOptions) {
|
||||
return request("PATCH", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP PUT request */
|
||||
export function put(url: string, options?: RequestOptions) {
|
||||
return request("PUT", url, options)
|
||||
}
|
||||
|
||||
/** Send HTTP DELETE request */
|
||||
export function del(url: string, options?: RequestOptions) {
|
||||
return request("DELETE", url, options)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
namespace settings {
|
||||
const RUN_KEY = "#run";
|
||||
const SCOPE_KEY = "#scope";
|
||||
const DEVICE_SECRETS_KEY = "#secrets";
|
||||
const SECRETS_KEY = "__secrets";
|
||||
|
||||
//% shim=pxt::seedAddRandom
|
||||
declare function seedAddRandom(n: number): void;
|
||||
|
||||
|
@ -21,22 +26,22 @@ namespace settings {
|
|||
declare function _list(prefix: string): string[];
|
||||
|
||||
export function runNumber() {
|
||||
return readNumber("#run") || 0
|
||||
return readNumber(RUN_KEY) || 0
|
||||
}
|
||||
|
||||
function setScope(scope: string) {
|
||||
if (!scope || scope.length > 100)
|
||||
control.panic(922)
|
||||
const currScope = readString("#scope")
|
||||
const currScope = readString(SCOPE_KEY)
|
||||
if (currScope != scope) {
|
||||
_userClean()
|
||||
writeString("#scope", scope)
|
||||
writeString(SCOPE_KEY, scope)
|
||||
}
|
||||
}
|
||||
|
||||
function initScopes() {
|
||||
const rn = runNumber() + 1
|
||||
writeNumber("#run", rn)
|
||||
writeNumber(RUN_KEY, rn)
|
||||
|
||||
seedAddRandom(control.deviceSerialNumber() & 0x7fffffff)
|
||||
seedAddRandom(rn)
|
||||
|
@ -151,4 +156,82 @@ namespace settings {
|
|||
export function exists(key: string) {
|
||||
return _exists(key)
|
||||
}
|
||||
|
||||
function clone(v: any): any {
|
||||
if (v == null) return null
|
||||
return JSON.parse(JSON.stringify(v))
|
||||
}
|
||||
|
||||
function isKV(v: any) {
|
||||
return !!v && typeof v === "object" && !Array.isArray(v)
|
||||
}
|
||||
|
||||
function jsonMergeFrom(trg: any, src: any) {
|
||||
if (!src) return;
|
||||
const keys = Object.keys(src)
|
||||
keys.forEach(k => {
|
||||
const srck = src[k];
|
||||
if (isKV(trg[k]) && isKV(srck))
|
||||
jsonMergeFrom(trg[k], srck);
|
||||
else
|
||||
trg[k] = clone(srck);
|
||||
});
|
||||
}
|
||||
|
||||
//% fixedInstances
|
||||
export class SecretStore {
|
||||
constructor(private key: string) { }
|
||||
|
||||
setSecret(name: string, value: any) {
|
||||
const secrets = this.readSecrets();
|
||||
secrets[name] = value;
|
||||
writeString(this.key, JSON.stringify(secrets));
|
||||
}
|
||||
|
||||
updateSecret(name: string, value: any) {
|
||||
const secrets = this.readSecrets();
|
||||
const secret = secrets[name];
|
||||
if (secret === undefined)
|
||||
secrets[name] = value;
|
||||
else jsonMergeFrom(secret, value);
|
||||
const v = JSON.stringify(secrets);
|
||||
writeString(this.key, v);
|
||||
}
|
||||
|
||||
readSecret(name: string, ensure: boolean = false): any {
|
||||
const secrets = this.readSecrets();
|
||||
const secret = secrets[name];
|
||||
if (ensure && !secret) {
|
||||
control.dmesg("missing secret " + name);
|
||||
control.panic(control.PXT_PANIC.SETTINGS_SECRET_MISSING);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
clearSecrets() {
|
||||
writeString(this.key, "{}");
|
||||
}
|
||||
|
||||
readSecrets(): any {
|
||||
try {
|
||||
const src = readString(this.key) || "{}";
|
||||
return JSON.parse(src) || {};
|
||||
} catch {
|
||||
control.dmesg("invalid secret format")
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secrets shared by any program on the device
|
||||
*/
|
||||
//% fixedInstance whenUsed block="device secrets"
|
||||
export const deviceSecrets = new SecretStore(DEVICE_SECRETS_KEY);
|
||||
|
||||
/**
|
||||
* Program secrets
|
||||
*/
|
||||
//% fixedInstance whenUsed block="program secrets"
|
||||
export const programSecrets = new SecretStore(SECRETS_KEY);
|
||||
}
|
||||
|
|
|
@ -43,8 +43,9 @@
|
|||
"libs/edge-connector",
|
||||
"libs/proximity",
|
||||
"libs/feather",
|
||||
"libs/esp32spi",
|
||||
"libs/esp32",
|
||||
"libs/net",
|
||||
"libs/net-game",
|
||||
"libs/mqtt",
|
||||
"libs/azureiot",
|
||||
"libs/color",
|
||||
|
|
Загрузка…
Ссылка в новой задаче