Home
JAQForum Ver 20.06
Log In or Join  
Active Topics
Local Time 00:18 21 Apr 2024 Privacy Policy
Jump to

Notice. New forum software under development. It's going to miss a few functions and look a bit ugly for a while, but I'm working on it full time now as the old forum was too unstable. Couple days, all good. If you notice any issues, please contact me.

Forum Index : Microcontroller and PC projects : SPI communication

     Page 1 of 2    
Author Message
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 08:26am 24 Jan 2022
Copy link to clipboard 
Print this post

SPI
Serial Peripheral Interface

SPI is designed to use a single master device and multiple slave devices. There are methods of using multiple master devices but they aren't covered here. Note that only one master device can produce the clk signal at any time.

SPI can be used in two ways:

A: Selected peripheral
All peripherals are connected to the rx, tx and clk lines in parallel. In addition each peripheral has a select line from the master to enable it onto the bus. The master produces the only clock signal, peripherals cannot do this.

B: Addressed peripherals
Peripherals are all connected to the clk line in parallel, but the tx of one is connected to the rx of the next, putting them in series. Multiple peripherals can be used in this way, the common clk signal keeping all data synchronised. Each peripheral has its own address, which forms part of the header message of the data. No select lines are necessary in this mode.

Data transfer is synchronised by the master's clk signal. Transmit and receive are synchronous - as one bit is clocked out on the tx line one is received on the rx line simultaneously. Thus 8 clk cycles will send and receive one byte. Each peripheral has a shift register which receives data from the rx pin at one end and transmits it from the tx pin at the other in response to the clk signal. As 8 clk cycles occur one byte will enter the shift register and one byte will be transmitted out from it at the other end. How long the shift register is depends on the peripheral.

If you have, for example, 3 peripherals in series. One has an 8-bit shift register and the other two have 16 bits each then the master will clock out 40 bits of data in a stream from its transmit buffer. That will replace the data in all three peripherals, the master receiving the contents of all three shift registers into its receive buffer. Note that none of the peripherals contains valid data until the entire 40 bits has been sent and received by the master.

Selected peripherals work in the same way. As data is clocked into them the existing content of their shift register is transmitted out of them and back to the master. Only on completion of filling the peripheral's shift register does it have valid data in it. The data received from an SPI peripheral is what was loaded into its shift register prior to the SPI transmission starting, thus you can't receive a reply to a command that is currently being transmitted.

The usage method is to send a command to a device, if necessary it must be padded to the length of the shift register. On completion of the command stage the peripheral will have transmitted the contents of its shift register - which have nothing to do with the command in progress. Then the master transmits another string the same length as the shift register. This might be another command or just a string of nulls. At the end of this second transmission the master will have received the response to the first command and the peripheral's shift register will hold the new transmission.

From the above you can probably see that SPI is designed for high speed, short distance communication of short messages. For sending large amounts of data, such as text for example, some sort of packet protocol is usually necessary unless you are transmitting a byte at a time as SPI cannot stream data longer than the length of the shift register in the peripheral. MMBasic has the facility to send and receive 4, 8 or 16 bits at a time.
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
lew247

Guru

Joined: 23/12/2015
Location: United Kingdom
Posts: 1676
Posted: 09:43am 24 Jan 2022
Copy link to clipboard 
Print this post

  Mixtel90 said  SPI
In addition each peripheral has a select line from the master to enable it onto the bus.

From the above you can probably see that SPI is designed for high speed, short distance communication of short messages. For sending large amounts of data, such as text for example, some sort of packet protocol is usually necessary unless you are transmitting a byte at a time as SPI cannot stream data longer than the length of the shift register in the peripheral. MMBasic has the facility to send and receive 4, 8 or 16 bits at a time.


Thats why I was asking Peter about adding the ability to have CS and Handshake to SPI
In the SD card example you choose the CS pin by OPTION SDCARD GP22 or whatever number pin is used

When sending strings to an attached device say for example an Esp8285 MMBasic has no facility I can see to address a CS pin other than if its an SD card

The Esp8285 also uses a handshake pin I guess this is to say its ok to send the next bit of data I have received the previous?

It works perfectly in micropython and even arudino so Im guessing it should in basic if I can find the correct way to do it

or like others have said do I just give up

The reason I want to persue this is because several boards are available with built in WiFi and although I have no projects in mind at all that will make use of them
I am certain if they can be made work with MMBasic then others will find it handy and use them in projects

The SeeedStudio one is pretty cheap under GB10, simple and has everything the official rp2040 board has minus 2 pins which are used for communication with the esp8285

It might turn out I bought a turd in which case I will just accept that, but only after Ive tried every possibiliy
Even if it means giving up on basic and using circuitpython or arduino as they do work

For anyone "really interested" here is an ESP32 co-processor board that connects to the main board by SPI complete with full code and examples on how to communicate with it
While its an ESP32 and obviously has specific firmware on it, the way the SPI talks to the ESP32 should be identical to what is needed for the esp8285

On a sideline I've just discovered that the Nina W102 ublox module as used in the Arduino Nano RP2040 is actually an ESP32 connected by SPI internally so it's compatible with the ESP32 SPI CircuitPython libraries with a few pin modifications.

as shown here

I do also have one of these boards to play with
Edited 2022-01-24 19:56 by lew247
 
thwill

Guru

Joined: 16/09/2019
Location: United Kingdom
Posts: 3833
Posted: 10:22am 24 Jan 2022
Copy link to clipboard 
Print this post

From another thread:

  JohnS said   (I gather he's looked hard & there is no working example & no spec.

Hopefully you do better...

However, as they will struggle to sell devices with such lack of support, why choose them? There are others which do have examples/specs.


Honestly because I'm not trying to achieve anything other than knowledge it doesn't really bother me. I'll just have a play and see what I learn. I imagine some of it will be transferable. If I end up helping Lewis along the way then that's even better.

Best wishes,

Tom
Game*Mite, CMM2 Welcome Tape, Creaky old text adventures
 
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 10:24am 24 Jan 2022
Copy link to clipboard 
Print this post

You can allocate any PicoMite pin you like to the CS function, it doesn't have to be a specific pin. It's normally high to disable the peripheral. Make it low, send your message, make it high again. As long as it's low only that device on the SPI bus is active, any with high CS pins are effectively disconnected.

You can't use handshaking back to the master *with SPI* as peripherals can't do anything unless they are being clocked and they can't produce the clock signal. There isn't any support for it in SPI itself. It simply doesn't work like that.

What you could do is to use a pin on an SPI peripheral to signal back to the master that data is available (or that it requires data - the idea is the same). The master would then lower the CS line to the peripheral and send a string of nulls to read the data. It would then raise the CS line again. The signal from the peripheral would have to be handled by that device. The master would have to look after any polling or interrupt on it's trigger pin.

You will have to look at data sheets for the devices in question. You have to know how many bits/bytes of data they are expecting otherwise you can't do anything over SPI. Each one will have, probably, a set of byte registers and you send bytes to them. In actual fact the data stream is in consecutive bits, so 40 bits might be 5x 8-bit registers on the chip. MMBasic makes it nice and easy - you just send 5 bytes and receive 5 bytes (the previous content of those registers). As I explained though, what you get back *while you are transmitting those 5 bytes* is of historical interest only and isn't related to the data you are sending.

If the devices on board are communicating with only 2 pins then it's most likely to be I2C, not SPI. SPI *always* has a clock signal but can't do tx and rx on the same pin. I suppose you could have tx or rx only systems, but they'd be unusual and not of much use.
Edited 2022-01-24 20:25 by Mixtel90
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
lew247

Guru

Joined: 23/12/2015
Location: United Kingdom
Posts: 1676
Posted: 11:22am 24 Jan 2022
Copy link to clipboard 
Print this post

  Mixtel90 said  
If the devices on board are communicating with only 2 pins then it's most likely to be I2C, not SPI. SPI *always* has a clock signal but can't do tx and rx on the same pin. I suppose you could have tx or rx only systems, but they'd be unusual and not of much use.


Definitely SPI
Miso GP8, MOSI GP11, CLK GP10, CS GP9, Handshake GP21
I said 2 pins I meant spi tx and rx
 
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 12:12pm 24 Jan 2022
Copy link to clipboard 
Print this post

The handshake has to be handled in software somehow. The SPI standard doesn't support that signal. CS is always controlled by the master device, so I assume that the handshake must work something like I described above. You can't think of it as RS-232 flow control as once the master has started sending data there is no valid data on the system until it's completed. Consequently there's no point in interrupting it's transmission. Unless someone in the know will let you know how it's working you'll have a few happy hours with a test rig and logic analyser.
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
lew247

Guru

Joined: 23/12/2015
Location: United Kingdom
Posts: 1676
Posted: 01:46pm 24 Jan 2022
Copy link to clipboard 
Print this post

I found this - it's for an Esp32 using SPI to communicate with another processor so I guess it should be the same for any SPI comms device?

It's Adafruits circuitpython for ESP SPI part of which is this
Hopefully someone who actually knows code properly will be able to say if this is of any help or not

class ESP_SPIcontrol:  # pylint: disable=too-many-public-methods, too-many-instance-attributes
   """A class that will talk to an ESP32 module programmed with special firmware
   that lets it act as a fast an efficient WiFi co-processor"""

   TCP_MODE = const(0)
   UDP_MODE = const(1)
   TLS_MODE = const(2)

   # pylint: disable=too-many-arguments
   def __init__(
       self, spi, cs_pin, ready_pin, reset_pin, gpio0_pin=None, *, debug=False
   ):
       self._debug = debug
       self.set_psk = False
       self.set_crt = False
       self._buffer = bytearray(10)
       self._pbuf = bytearray(1)  # buffer for param read
       self._sendbuf = bytearray(256)  # buffer for command sending
       self._socknum_ll = [[0]]  # pre-made list of list of socket #

       self._spi_device = SPIDevice(spi, cs_pin, baudrate=8000000)
       self._cs = cs_pin
       self._ready = ready_pin
       self._reset = reset_pin
       self._gpio0 = gpio0_pin
       self._cs.direction = Direction.OUTPUT
       self._ready.direction = Direction.INPUT
       self._reset.direction = Direction.OUTPUT
       # Only one TLS socket at a time is supported so track when we already have one.
       self._tls_socket = None
       if self._gpio0:
           self._gpio0.direction = Direction.INPUT
       self.reset()

   # pylint: enable=too-many-arguments

   def reset(self):
       """Hard reset the ESP32 using the reset pin"""
       if self._debug:
           print("Reset ESP32")
       if self._gpio0:
           self._gpio0.direction = Direction.OUTPUT
           self._gpio0.value = True  # not bootload mode
       self._cs.value = True
       self._reset.value = False
       time.sleep(0.01)  # reset
       self._reset.value = True
       time.sleep(0.75)  # wait for it to boot up
       if self._gpio0:
           self._gpio0.direction = Direction.INPUT

   def _wait_for_ready(self):
       """Wait until the ready pin goes low"""
       if self._debug >= 3:
           print("Wait for ESP32 ready", end="")
       times = time.monotonic()
       while (time.monotonic() - times) < 10:  # wait up to 10 seconds
           if not self._ready.value:  # we're ready!
               break
           if self._debug >= 3:
               print(".", end="")
               time.sleep(0.05)
       else:
           raise RuntimeError("ESP32 not responding")
       if self._debug >= 3:
           print()

   # pylint: disable=too-many-branches
   def _send_command(self, cmd, params=None, *, param_len_16=False):
       """Send over a command with a list of parameters"""
       if not params:
           params = ()

       packet_len = 4  # header + end byte
       for i, param in enumerate(params):
           packet_len += len(param)  # parameter
           packet_len += 1  # size byte
           if param_len_16:
               packet_len += 1  # 2 of em here!
       while packet_len % 4 != 0:
           packet_len += 1
       # we may need more space
       if packet_len > len(self._sendbuf):
           self._sendbuf = bytearray(packet_len)

       self._sendbuf[0] = _START_CMD
       self._sendbuf[1] = cmd & ~_REPLY_FLAG
       self._sendbuf[2] = len(params)

       # handle parameters here
       ptr = 3
       for i, param in enumerate(params):
           if self._debug >= 2:
               print("\tSending param #%d is %d bytes long" % (i, len(param)))
           if param_len_16:
               self._sendbuf[ptr] = (len(param) >> 8) & 0xFF
               ptr += 1
           self._sendbuf[ptr] = len(param) & 0xFF
           ptr += 1
           for j, par in enumerate(param):
               self._sendbuf[ptr + j] = par
           ptr += len(param)
       self._sendbuf[ptr] = _END_CMD

       self._wait_for_ready()
       with self._spi_device as spi:
           times = time.monotonic()
           while (time.monotonic() - times) < 1:  # wait up to 1000ms
               if self._ready.value:  # ok ready to send!
                   break
           else:
               raise RuntimeError("ESP32 timed out on SPI select")
           spi.write(
               self._sendbuf, start=0, end=packet_len
           )  # pylint: disable=no-member
           if self._debug >= 3:
               print("Wrote: ", [hex(b) for b in self._sendbuf[0:packet_len]])

   # pylint: disable=too-many-branches

   def _read_byte(self, spi):
       """Read one byte from SPI"""
       spi.readinto(self._pbuf)
       if self._debug >= 3:
           print("\t\tRead:", hex(self._pbuf[0]))
       return self._pbuf[0]

   def _read_bytes(self, spi, buffer, start=0, end=None):
       """Read many bytes from SPI"""
       if not end:
           end = len(buffer)
       spi.readinto(buffer, start=start, end=end)
       if self._debug >= 3:
           print("\t\tRead:", [hex(i) for i in buffer])

   def _wait_spi_char(self, spi, desired):
       """Read a byte with a retry loop, and if we get it, check that its what we expect"""
       for _ in range(10):
           r = self._read_byte(spi)
           if r == _ERR_CMD:
               raise RuntimeError("Error response to command")
           if r == desired:
               return True
           time.sleep(0.01)
       raise RuntimeError("Timed out waiting for SPI char")

   def _check_data(self, spi, desired):
       """Read a byte and verify its the value we want"""
       r = self._read_byte(spi)
       if r != desired:
           raise RuntimeError("Expected %02X but got %02X" % (desired, r))

   def _wait_response_cmd(self, cmd, num_responses=None, *, param_len_16=False):
       """Wait for ready, then parse the response"""
       self._wait_for_ready()

       responses = []
       with self._spi_device as spi:
           times = time.monotonic()
           while (time.monotonic() - times) < 1:  # wait up to 1000ms
               if self._ready.value:  # ok ready to send!
                   break
           else:
               raise RuntimeError("ESP32 timed out on SPI select")

           self._wait_spi_char(spi, _START_CMD)
           self._check_data(spi, cmd | _REPLY_FLAG)
           if num_responses is not None:
               self._check_data(spi, num_responses)
           else:
               num_responses = self._read_byte(spi)
           for num in range(num_responses):
               param_len = self._read_byte(spi)
               if param_len_16:
                   param_len <<= 8
                   param_len |= self._read_byte(spi)
               if self._debug >= 2:
                   print("\tParameter #%d length is %d" % (num, param_len))
               response = bytearray(param_len)
               self._read_bytes(spi, response)
               responses.append(response)
           self._check_data(spi, _END_CMD)

       if self._debug >= 2:
           print("Read %d: " % len(responses[0]), responses)
       return responses

   def _send_command_get_response(
       self,
       cmd,
       params=None,
       *,
       reply_params=1,
       sent_param_len_16=False,
       recv_param_len_16=False
   ):
       """Send a high level SPI command, wait and return the response"""
       self._send_command(cmd, params, param_len_16=sent_param_len_16)
       return self._wait_response_cmd(
           cmd, reply_params, param_len_16=recv_param_len_16
       )

   @property
   def status(self):
       """The status of the ESP32 WiFi core. Can be WL_NO_SHIELD or WL_NO_MODULE
       (not found), WL_IDLE_STATUS, WL_NO_SSID_AVAIL, WL_SCAN_COMPLETED,
       WL_CONNECTED, WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED,
       WL_AP_LISTENING, WL_AP_CONNECTED, WL_AP_FAILED"""
       resp = self._send_command_get_response(_GET_CONN_STATUS_CMD)
       if self._debug:
           print("Connection status:", resp[0][0])
       return resp[0][0]  # one byte response

   @property
   def firmware_version(self):
       """A string of the firmware version on the ESP32"""
       if self._debug:
           print("Firmware version")
       resp = self._send_command_get_response(_GET_FW_VERSION_CMD)
       return resp[0]

   @property
   def MAC_address(self):  # pylint: disable=invalid-name
       """A bytearray containing the MAC address of the ESP32"""
       if self._debug:
           print("MAC address")
       resp = self._send_command_get_response(_GET_MACADDR_CMD, [b"\xFF"])
       return resp[0]

   @property
   def MAC_address_actual(self):  # pylint: disable=invalid-name
       """A bytearray containing the actual MAC address of the ESP32"""
       if self._debug:
           print("MAC address")
       resp = self._send_command_get_response(_GET_MACADDR_CMD, [b"\xFF"])
       new_resp = bytearray(resp[0])
       new_resp = reversed(new_resp)
       return new_resp

   def start_scan_networks(self):
       """Begin a scan of visible access points. Follow up with a call
       to 'get_scan_networks' for response"""
       if self._debug:
           print("Start scan")
       resp = self._send_command_get_response(_START_SCAN_NETWORKS)
       if resp[0][0] != 1:
           raise RuntimeError("Failed to start AP scan")

   def get_scan_networks(self):
       """The results of the latest SSID scan. Returns a list of dictionaries with
       'ssid', 'rssi', 'encryption', bssid, and channel entries, one for each AP found"""
       self._send_command(_SCAN_NETWORKS)
       names = self._wait_response_cmd(_SCAN_NETWORKS)
       # print("SSID names:", names)
       APs = []  # pylint: disable=invalid-name
       for i, name in enumerate(names):
           a_p = {"ssid": name}
           rssi = self._send_command_get_response(_GET_IDX_RSSI_CMD, ((i,),))[0]
           a_p["rssi"] = struct.unpack("<i", rssi)[0]
           encr = self._send_command_get_response(_GET_IDX_ENCT_CMD, ((i,),))[0]
           a_p["encryption"] = encr[0]
           bssid = self._send_command_get_response(_GET_IDX_BSSID_CMD, ((i,),))[0]
           a_p["bssid"] = bssid
           chan = self._send_command_get_response(_GET_IDX_CHAN_CMD, ((i,),))[0]
           a_p["channel"] = chan[0]
           APs.append(a_p)
       return APs

   def scan_networks(self):
       """Scan for visible access points, returns a list of access point details.
       Returns a list of dictionaries with 'ssid', 'rssi' and 'encryption' entries,
       one for each AP found"""
       self.start_scan_networks()
       for _ in range(10):  # attempts
           time.sleep(2)
           APs = self.get_scan_networks()  # pylint: disable=invalid-name
           if APs:
               return APs
       return None

   def wifi_set_network(self, ssid):
       """Tells the ESP32 to set the access point to the given ssid"""
       resp = self._send_command_get_response(_SET_NET_CMD, [ssid])
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set network")

   def wifi_set_passphrase(self, ssid, passphrase):
       """Sets the desired access point ssid and passphrase"""
       resp = self._send_command_get_response(_SET_PASSPHRASE_CMD, [ssid, passphrase])
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set passphrase")

   def wifi_set_entidentity(self, ident):
       """Sets the WPA2 Enterprise anonymous identity"""
       resp = self._send_command_get_response(_SET_ENT_IDENT_CMD, [ident])
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set enterprise anonymous identity")

   def wifi_set_entusername(self, username):
       """Sets the desired WPA2 Enterprise username"""
       resp = self._send_command_get_response(_SET_ENT_UNAME_CMD, [username])
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set enterprise username")

   def wifi_set_entpassword(self, password):
       """Sets the desired WPA2 Enterprise password"""
       resp = self._send_command_get_response(_SET_ENT_PASSWD_CMD, [password])
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set enterprise password")

   def wifi_set_entenable(self):
       """Enables WPA2 Enterprise mode"""
       resp = self._send_command_get_response(_SET_ENT_ENABLE_CMD)
       if resp[0][0] != 1:
           raise RuntimeError("Failed to enable enterprise mode")

   def _wifi_set_ap_network(self, ssid, channel):
       """Creates an Access point with SSID and Channel"""
       resp = self._send_command_get_response(_SET_AP_NET_CMD, [ssid, channel])
       if resp[0][0] != 1:
           raise RuntimeError("Failed to setup AP network")

   def _wifi_set_ap_passphrase(self, ssid, passphrase, channel):
       """Creates an Access point with SSID, passphrase, and Channel"""
       resp = self._send_command_get_response(
           _SET_AP_PASSPHRASE_CMD, [ssid, passphrase, channel]
       )
       if resp[0][0] != 1:
           raise RuntimeError("Failed to setup AP password")

   @property
   def ssid(self):
       """The name of the access point we're connected to"""
       resp = self._send_command_get_response(_GET_CURR_SSID_CMD, [b"\xFF"])
       return resp[0]

   @property
   def bssid(self):
       """The MAC-formatted service set ID of the access point we're connected to"""
       resp = self._send_command_get_response(_GET_CURR_BSSID_CMD, [b"\xFF"])
       return resp[0]

   @property
   def rssi(self):
       """The receiving signal strength indicator for the access point we're
       connected to"""
       resp = self._send_command_get_response(_GET_CURR_RSSI_CMD, [b"\xFF"])
       return struct.unpack("<i", resp[0])[0]

   @property
   def network_data(self):
       """A dictionary containing current connection details such as the 'ip_addr',
       'netmask' and 'gateway'"""
       resp = self._send_command_get_response(
           _GET_IPADDR_CMD, [b"\xFF"], reply_params=3
       )
       return {"ip_addr": resp[0], "netmask": resp[1], "gateway": resp[2]}

   @property
   def ip_address(self):
       """Our local IP address"""
       return self.network_data["ip_addr"]

   @property
   def is_connected(self):
       """Whether the ESP32 is connected to an access point"""
       try:
           return self.status == WL_CONNECTED
       except RuntimeError:
           self.reset()
           return False

   @property
   def ap_listening(self):
       """Returns if the ESP32 is in access point mode and is listening for connections"""
       try:
           return self.status == WL_AP_LISTENING
       except RuntimeError:
           self.reset()
           return False

   def disconnect(self):
       """Disconnect from the access point"""
       resp = self._send_command_get_response(_DISCONNECT_CMD)
       if resp[0][0] != 1:
           raise RuntimeError("Failed to disconnect")

   def connect(self, secrets):
       """Connect to an access point using a secrets dictionary
       that contains a 'ssid' and 'password' entry"""
       self.connect_AP(secrets["ssid"], secrets["password"])

   def connect_AP(self, ssid, password, timeout_s=10):  # pylint: disable=invalid-name
       """
       Connect to an access point with given name and password.
       Will wait until specified timeout seconds and return on success
       or raise an exception on failure.
       :param ssid: the SSID to connect to
       :param passphrase: the password of the access point
       :param timeout_s: number of seconds until we time out and fail to create AP
       """
       if self._debug:
           print("Connect to AP", ssid, password)
       if isinstance(ssid, str):
           ssid = bytes(ssid, "utf-8")
       if password:
           if isinstance(password, str):
               password = bytes(password, "utf-8")
           self.wifi_set_passphrase(ssid, password)
       else:
           self.wifi_set_network(ssid)
       times = time.monotonic()
       while (time.monotonic() - times) < timeout_s:  # wait up until timeout
           stat = self.status
           if stat == WL_CONNECTED:
               return stat
           time.sleep(0.05)
       if stat in (WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED):
           raise RuntimeError("Failed to connect to ssid", ssid)
       if stat == WL_NO_SSID_AVAIL:
           raise RuntimeError("No such ssid", ssid)
       raise RuntimeError("Unknown error 0x%02X" % stat)

   def create_AP(
       self, ssid, password, channel=1, timeout=10
   ):  # pylint: disable=invalid-name
       """
       Create an access point with the given name, password, and channel.
       Will wait until specified timeout seconds and return on success
       or raise an exception on failure.
       :param str ssid: the SSID of the created Access Point. Must be less than 32 chars.
       :param str password: the password of the created Access Point. Must be 8-63 chars.
       :param int channel: channel of created Access Point (1 - 14).
       :param int timeout: number of seconds until we time out and fail to create AP
       """
       if len(ssid) > 32:
           raise RuntimeError("ssid must be no more than 32 characters")
       if password and (len(password) < 8 or len(password) > 64):
           raise RuntimeError("password must be 8 - 63 characters")
       if channel < 1 or channel > 14:
           raise RuntimeError("channel must be between 1 and 14")

       if isinstance(channel, int):
           channel = bytes(channel)
       if isinstance(ssid, str):
           ssid = bytes(ssid, "utf-8")
       if password:
           if isinstance(password, str):
               password = bytes(password, "utf-8")
           self._wifi_set_ap_passphrase(ssid, password, channel)
       else:
           self._wifi_set_ap_network(ssid, channel)

       times = time.monotonic()
       while (time.monotonic() - times) < timeout:  # wait up to timeout
           stat = self.status
           if stat == WL_AP_LISTENING:
               return stat
           time.sleep(0.05)
       if stat == WL_AP_FAILED:
           raise RuntimeError("Failed to create AP", ssid)
       raise RuntimeError("Unknown error 0x%02x" % stat)

   def pretty_ip(self, ip):  # pylint: disable=no-self-use, invalid-name
       """Converts a bytearray IP address to a dotted-quad string for printing"""
       return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3])

   def unpretty_ip(self, ip):  # pylint: disable=no-self-use, invalid-name
       """Converts a dotted-quad string to a bytearray IP address"""
       octets = [int(x) for x in ip.split(".")]
       return bytes(octets)

   def get_host_by_name(self, hostname):
       """Convert a hostname to a packed 4-byte IP address. Returns
       a 4 bytearray"""
       if self._debug:
           print("*** Get host by name")
       if isinstance(hostname, str):
           hostname = bytes(hostname, "utf-8")
       resp = self._send_command_get_response(_REQ_HOST_BY_NAME_CMD, (hostname,))
       if resp[0][0] != 1:
           raise RuntimeError("Failed to request hostname")
       resp = self._send_command_get_response(_GET_HOST_BY_NAME_CMD)
       return resp[0]

   def ping(self, dest, ttl=250):
       """Ping a destination IP address or hostname, with a max time-to-live
       (ttl). Returns a millisecond timing value"""
       if isinstance(dest, str):  # convert to IP address
           dest = self.get_host_by_name(dest)
       # ttl must be between 0 and 255
       ttl = max(0, min(ttl, 255))
       resp = self._send_command_get_response(_PING_CMD, (dest, (ttl,)))
       return struct.unpack("<H", resp[0])[0]

   def get_socket(self):
       """Request a socket from the ESP32, will allocate and return a number that
       can then be passed to the other socket commands"""
       if self._debug:
           print("*** Get socket")
       resp = self._send_command_get_response(_GET_SOCKET_CMD)
       resp = resp[0][0]
       if resp == 255:
           raise OSError(23)  # ENFILE - File table overflow
       if self._debug:
           print("Allocated socket #%d" % resp)
       return resp

   def socket_open(self, socket_num, dest, port, conn_mode=TCP_MODE):
       """Open a socket to a destination IP address or hostname
       using the ESP32's internal reference number. By default we use
       'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE
       (dest must be hostname for TLS_MODE!)"""
       self._socknum_ll[0][0] = socket_num
       if self._debug:
           print("*** Open socket to", dest, port, conn_mode)
       if conn_mode == ESP_SPIcontrol.TLS_MODE and self._tls_socket is not None:
           raise OSError(23)  # ENFILE - File table overflow
       port_param = struct.pack(">H", port)
       if isinstance(dest, str):  # use the 5 arg version
           dest = bytes(dest, "utf-8")
           resp = self._send_command_get_response(
               _START_CLIENT_TCP_CMD,
               (
                   dest,
                   b"\x00\x00\x00\x00",
                   port_param,
                   self._socknum_ll[0],
                   (conn_mode,),
               ),
           )
       else:  # ip address, use 4 arg vesion
           resp = self._send_command_get_response(
               _START_CLIENT_TCP_CMD,
               (dest, port_param, self._socknum_ll[0], (conn_mode,)),
           )
       if resp[0][0] != 1:
           raise RuntimeError("Could not connect to remote server")
       if conn_mode == ESP_SPIcontrol.TLS_MODE:
           self._tls_socket = socket_num

   def socket_status(self, socket_num):
       """Get the socket connection status, can be SOCKET_CLOSED, SOCKET_LISTEN,
       SOCKET_SYN_SENT, SOCKET_SYN_RCVD, SOCKET_ESTABLISHED, SOCKET_FIN_WAIT_1,
       SOCKET_FIN_WAIT_2, SOCKET_CLOSE_WAIT, SOCKET_CLOSING, SOCKET_LAST_ACK, or
       SOCKET_TIME_WAIT"""
       self._socknum_ll[0][0] = socket_num
       resp = self._send_command_get_response(
           _GET_CLIENT_STATE_TCP_CMD, self._socknum_ll
       )
       return resp[0][0]

   def socket_connected(self, socket_num):
       """Test if a socket is connected to the destination, returns boolean true/false"""
       return self.socket_status(socket_num) == SOCKET_ESTABLISHED

   def socket_write(self, socket_num, buffer, conn_mode=TCP_MODE):
       """Write the bytearray buffer to a socket"""
       if self._debug:
           print("Writing:", buffer)
       self._socknum_ll[0][0] = socket_num
       sent = 0
       total_chunks = (len(buffer) // 64) + 1
       send_command = _SEND_DATA_TCP_CMD
       if conn_mode == self.UDP_MODE:  # UDP requires a different command to write
           send_command = _INSERT_DATABUF_TCP_CMD
       for chunk in range(total_chunks):
           resp = self._send_command_get_response(
               send_command,
               (
                   self._socknum_ll[0],
                   memoryview(buffer)[(chunk * 64) : ((chunk + 1) * 64)],
               ),
               sent_param_len_16=True,
           )
           sent += resp[0][0]

       if conn_mode == self.UDP_MODE:
           # UDP verifies chunks on write, not bytes
           if sent != total_chunks:
               raise RuntimeError(
                   "Failed to write %d chunks (sent %d)" % (total_chunks, sent)
               )
           # UDP needs to finalize with this command, does the actual sending
           resp = self._send_command_get_response(_SEND_UDP_DATA_CMD, self._socknum_ll)
           if resp[0][0] != 1:
               raise RuntimeError("Failed to send UDP data")
           return

       if sent != len(buffer):
           self.socket_close(socket_num)
           raise RuntimeError(
               "Failed to send %d bytes (sent %d)" % (len(buffer), sent)
           )

       resp = self._send_command_get_response(_DATA_SENT_TCP_CMD, self._socknum_ll)
       if resp[0][0] != 1:
           raise RuntimeError("Failed to verify data sent")

   def socket_available(self, socket_num):
       """Determine how many bytes are waiting to be read on the socket"""
       self._socknum_ll[0][0] = socket_num
       resp = self._send_command_get_response(_AVAIL_DATA_TCP_CMD, self._socknum_ll)
       reply = struct.unpack("<H", resp[0])[0]
       if self._debug:
           print("ESPSocket: %d bytes available" % reply)
       return reply

   def socket_read(self, socket_num, size):
       """Read up to 'size' bytes from the socket number. Returns a bytearray"""
       if self._debug:
           print(
               "Reading %d bytes from ESP socket with status %d"
               % (size, self.socket_status(socket_num))
           )
       self._socknum_ll[0][0] = socket_num
       resp = self._send_command_get_response(
           _GET_DATABUF_TCP_CMD,
           (self._socknum_ll[0], (size & 0xFF, (size >> 8) & 0xFF)),
           sent_param_len_16=True,
           recv_param_len_16=True,
       )
       return bytes(resp[0])

   def socket_connect(self, socket_num, dest, port, conn_mode=TCP_MODE):
       """Open and verify we connected a socket to a destination IP address or hostname
       using the ESP32's internal reference number. By default we use
       'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE (dest must
       be hostname for TLS_MODE!)"""
       if self._debug:
           print("*** Socket connect mode", conn_mode)

       self.socket_open(socket_num, dest, port, conn_mode=conn_mode)
       if conn_mode == self.UDP_MODE:
           # UDP doesn't actually establish a connection
           # but the socket for writing is created via start_server
           self.start_server(port, socket_num, conn_mode)
           return True

       times = time.monotonic()
       while (time.monotonic() - times) < 3:  # wait 3 seconds
           if self.socket_connected(socket_num):
               return True
           time.sleep(0.01)
       raise RuntimeError("Failed to establish connection")

   def socket_close(self, socket_num):
       """Close a socket using the ESP32's internal reference number"""
       if self._debug:
           print("*** Closing socket #%d" % socket_num)
       self._socknum_ll[0][0] = socket_num
       try:
           self._send_command_get_response(_STOP_CLIENT_TCP_CMD, self._socknum_ll)
       except RuntimeError:
           pass
       if socket_num == self._tls_socket:
           self._tls_socket = None

   def start_server(
       self, port, socket_num, conn_mode=TCP_MODE, ip=None
   ):  # pylint: disable=invalid-name
       """Opens a server on the specified port, using the ESP32's internal reference number"""
       if self._debug:
           print("*** starting server")
       self._socknum_ll[0][0] = socket_num
       params = [struct.pack(">H", port), self._socknum_ll[0], (conn_mode,)]
       if ip:
           params.insert(0, ip)
       resp = self._send_command_get_response(_START_SERVER_TCP_CMD, params)

       if resp[0][0] != 1:
           raise RuntimeError("Could not start server")

   def server_state(self, socket_num):
       """Get the state of the ESP32's internal reference server socket number"""
       self._socknum_ll[0][0] = socket_num
       resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll)
       return resp[0][0]

   def set_esp_debug(self, enabled):
       """Enable/disable debug mode on the ESP32. Debug messages will be
       written to the ESP32's UART."""
       resp = self._send_command_get_response(_SET_DEBUG_CMD, ((bool(enabled),),))
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set debug mode")

   def set_pin_mode(self, pin, mode):
       """
       Set the io mode for a GPIO pin.
       :param int pin: ESP32 GPIO pin to set.
       :param value: direction for pin, digitalio.Direction or integer (0=input, 1=output).
       """
       if mode == Direction.OUTPUT:
           pin_mode = 1
       elif mode == Direction.INPUT:
           pin_mode = 0
       else:
           pin_mode = mode
       resp = self._send_command_get_response(_SET_PIN_MODE_CMD, ((pin,), (pin_mode,)))
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set pin mode")

   def set_digital_write(self, pin, value):
       """
       Set the digital output value of pin.
       :param int pin: ESP32 GPIO pin to write to.
       :param bool value: Value for the pin.
       """
       resp = self._send_command_get_response(
           _SET_DIGITAL_WRITE_CMD, ((pin,), (value,))
       )
       if resp[0][0] != 1:
           raise RuntimeError("Failed to write to pin")

   def set_analog_write(self, pin, analog_value):
       """
       Set the analog output value of pin, using PWM.
       :param int pin: ESP32 GPIO pin to write to.
       :param float value: 0=off 1.0=full on
       """
       value = int(255 * analog_value)
       resp = self._send_command_get_response(
           _SET_ANALOG_WRITE_CMD, ((pin,), (value,))
       )
       if resp[0][0] != 1:
           raise RuntimeError("Failed to write to pin")

   def set_digital_read(self, pin):
       """
       Get the digital input value of pin. Returns the boolean value of the pin.
       :param int pin: ESP32 GPIO pin to read from.
       """
       # Verify nina-fw => 1.5.0
       fw_semver_maj = bytes(self.firmware_version).decode("utf-8")[2]
       assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above."

       resp = self._send_command_get_response(_SET_DIGITAL_READ_CMD, ((pin,),))[0]
       if resp[0] == 0:
           return False
       if resp[0] == 1:
           return True
       raise ValueError(
           "_SET_DIGITAL_READ response error: response is not boolean", resp[0]
       )

   def set_analog_read(self, pin, atten=ADC_ATTEN_DB_11):
       """
       Get the analog input value of pin. Returns an int between 0 and 65536.
       :param int pin: ESP32 GPIO pin to read from.
       :param int atten: attenuation constant
       """
       # Verify nina-fw => 1.5.0
       fw_semver_maj = bytes(self.firmware_version).decode("utf-8")[2]
       assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above."

       resp = self._send_command_get_response(_SET_ANALOG_READ_CMD, ((pin,), (atten,)))
       resp_analog = struct.unpack("<i", resp[0])
       if resp_analog[0] < 0:
           raise ValueError(
               "_SET_ANALOG_READ parameter error: invalid pin", resp_analog[0]
           )
       if self._debug:
           print(resp, resp_analog, resp_analog[0], 16 * resp_analog[0])
       return 16 * resp_analog[0]

   def get_time(self):
       """The current unix timestamp"""
       if self.status == WL_CONNECTED:
           resp = self._send_command_get_response(_GET_TIME)
           resp_time = struct.unpack("<i", resp[0])
           if resp_time == (0,):
               raise ValueError("_GET_TIME returned 0")
           return resp_time
       if self.status in (WL_AP_LISTENING, WL_AP_CONNECTED):
           raise RuntimeError(
               "Cannot obtain NTP while in AP mode, must be connected to internet"
           )
       raise RuntimeError("Must be connected to WiFi before obtaining NTP.")

   def set_certificate(self, client_certificate):
       """Sets client certificate. Must be called
       BEFORE a network connection is established.
       :param str client_certificate: User-provided .PEM certificate up to 1300 bytes.
       """
       if self._debug:
           print("** Setting client certificate")
       if self.status == WL_CONNECTED:
           raise RuntimeError(
               "set_certificate must be called BEFORE a connection is established."
           )
       if isinstance(client_certificate, str):
           client_certificate = bytes(client_certificate, "utf-8")
       if "-----BEGIN CERTIFICATE" not in client_certificate:
           raise TypeError(".PEM must start with -----BEGIN CERTIFICATE")
       assert len(client_certificate) < 1300, ".PEM must be less than 1300 bytes."
       resp = self._send_command_get_response(_SET_CLI_CERT, (client_certificate,))
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set client certificate")
       self.set_crt = True
       return resp[0]

   def set_private_key(self, private_key):
       """Sets private key. Must be called
       BEFORE a network connection is established.
       :param str private_key: User-provided .PEM file up to 1700 bytes.
       """
       if self._debug:
           print("** Setting client's private key.")
       if self.status == WL_CONNECTED:
           raise RuntimeError(
               "set_private_key must be called BEFORE a connection is established."
           )
       if isinstance(private_key, str):
           private_key = bytes(private_key, "utf-8")
       if "-----BEGIN RSA" not in private_key:
           raise TypeError(".PEM must start with -----BEGIN RSA")
       assert len(private_key) < 1700, ".PEM must be less than 1700 bytes."
       resp = self._send_command_get_response(_SET_PK, (private_key,))
       if resp[0][0] != 1:
           raise RuntimeError("Failed to set private key.")
       self.set_psk = True
       return resp[0]
 
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 02:13pm 24 Jan 2022
Copy link to clipboard 
Print this post

But why use SPI at all? It's a very fast, short range system for multiple devices. It's also not very forgiving. If you are only communicating between two devices then I2C or UART (especially!) would be far easier to get going. Having a high speed interface won't speed up the wifi communication in the least, it'll just fill buffers more quickly.
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
CaptainBoing

Guru

Joined: 07/09/2016
Location: United Kingdom
Posts: 1985
Posted: 02:26pm 24 Jan 2022
Copy link to clipboard 
Print this post

don't know but perhaps using SPI dispenses with all that tedious Hayes command set parsing - it can be a pain sometimes, mainly caused by it ('s origins) being designed to be read by humans and not consistently parse-able.

If I had the choice of direct commands using SPI and UART+parsing, I'd probably go with the former.
Edited 2022-01-25 00:26 by CaptainBoing
 
lew247

Guru

Joined: 23/12/2015
Location: United Kingdom
Posts: 1676
Posted: 02:29pm 24 Jan 2022
Copy link to clipboard 
Print this post

  Mixtel90 said  But why use SPI at all? It's a very fast, short range system for multiple devices.

BECAUSE it's what the various ESP8285/ESP32 co processor boards use to communicate with the main processor
and instead of inventing the wheel, it's easier to use a ready made wheel and choose your own tyre

  Quote  We placed an ESP32 module on a PCB with level shifting circuitry, a 3.3V regulator, and a tri-state chip for MOSI so you can share the SPI bus with other devices. Comes fully assembled and tested, pre-programmed with ESP32 SPI WiFi co-processor firmware that you can use in CircuitPython to use this into a WiFi co-processsor over SPI + 2 pins.

I really don't understand why it's so hard to understand that I'd like to use what's avaliale rather than have to make a boards or "bodge together" an esp8266 that isn't supported by the manufacturer anymore
Edited 2022-01-25 00:32 by lew247
 
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 02:47pm 24 Jan 2022
Copy link to clipboard 
Print this post

We'd like you to be able to do that - unfortunately, unless you can find people with a lot of experience with those boards, you are not in a good position to do so.
You *have* to know how the handshake works - and why it's there at all. You need a timing diagram for that really.
You *have* to know all the necessary commands *and* what the various responses can be.
You *have* to have an understanding of SPI.

A good start would be to learn CircuitPython then you'll be able to understand any libraries used and how they work. I realise that's a scary thing, but without that level of knowledge you can't convert the Python listing into MMBasic as there are no such libraries available. You're on your own.

You aren't setting off with a "ready made wheel" here. You're setting off with a semi-closed design by someone else and you're hoping to duplicate it.

If those boards are so popular and easy to use the web must be flooded with info on them. In actual fact, it's bad enough trying to find reliable info on the ESP8266 which, I'm told, *does* work (although that's not been my experience with them).
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
CaptainBoing

Guru

Joined: 07/09/2016
Location: United Kingdom
Posts: 1985
Posted: 03:47pm 24 Jan 2022
Copy link to clipboard 
Print this post

  Mixtel90 said  ... ESP8266 which, I'm told, *does* work (although that's not been my experience with them).

what was your problem? I had none. http://www.fruitoftheshed.com/MMBasic.ESP8266-Module-with-HTTP-API.ashx
 
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 04:12pm 24 Jan 2022
Copy link to clipboard 
Print this post

Lots. :)
I got two. At first they seemed to be sort of functional. I could work with AT commands up to a point, but wifi connections weren't very stable (and range isn't the problem). I flashed one with much later software (both had old versions) and from then on connections got even worse, only staying connected for a few seconds - if it would connect at all. I gave up at that point as I got interested in other things. :) I might go back at some point, but I need an incentive to do so. lol
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
CaptainBoing

Guru

Joined: 07/09/2016
Location: United Kingdom
Posts: 1985
Posted: 04:24pm 24 Jan 2022
Copy link to clipboard 
Print this post

hmmm... was this as client or server? Was it HTTP? remember HTTP is session-less, it will disconnect immediately after the transfer - that is "as-designed"

I am genuinely curious as I have not had any problems. I used one as both server and client in the same application and didn't have. Wonder if it is the firmware I had and I was lucky?
 
lizby
Guru

Joined: 17/05/2016
Location: United States
Posts: 3010
Posted: 05:00pm 24 Jan 2022
Copy link to clipboard 
Print this post

  Mixtel90 said  ... ESP8266 which, I'm told, *does* work (although that's not been my experience with them).


I also wonder. I've used them stand-alone (ESP-01, ESP D1 Mini, ESP32)--usually just for testing purposes, but I've also used them embedded in other devices, particularly Sonoff switches, with the ESP re-flashed with other firmware--Annex or Tasmota. These have been in use for long periods--several years for at least one of them.

I also have a D1 Mini running Annex which monitors the temperature on three hot water heat zone pipes. That failed for unknown reasons after over a year, but worked again when re-flashed.

I do not have any long-running examples which use the AT firmware, but have tested with it successfully.
PicoMite, Armmite F4, SensorKits, MMBasic Hardware, Games, etc. on fruitoftheshed
 
Mixtel90

Guru

Joined: 05/10/2019
Location: United Kingdom
Posts: 5707
Posted: 05:03pm 24 Jan 2022
Copy link to clipboard 
Print this post

TBH I can't remember much about what I was attempting at the time. I'd never used that sort of stuff before (and I still haven't) so it was a case of trying to follow instructions pretty blindly. The last thing I did was to make a little adapter so that I could plug one into a breadboard. :)

All this was before I started thinking about the Backpack, I put a socket on that so that it could be used for experimenting but I never got round to doing anything with it.
Edited 2022-01-25 03:05 by Mixtel90
Mick

Zilog Inside! nascom.info for Nascom & Gemini
Preliminary MMBasic docs & my PCB designs
 
JohnS
Guru

Joined: 18/11/2011
Location: United Kingdom
Posts: 3649
Posted: 06:09pm 24 Jan 2022
Copy link to clipboard 
Print this post

  thwill said  From another thread:

  JohnS said   (I gather he's looked hard & there is no working example & no spec.

Hopefully you do better...

However, as they will struggle to sell devices with such lack of support, why choose them? There are others which do have examples/specs.


Honestly because I'm not trying to achieve anything other than knowledge it doesn't really bother me. I'll just have a play and see what I learn. I imagine some of it will be transferable. If I end up helping Lewis along the way then that's even better.

Best wishes,

Tom

That's all good.

What's weird is that Lewis does (now) seem to have found code, so should just require equivalent stuff.

John
 
bigfix
Senior Member

Joined: 20/02/2014
Location: Austria
Posts: 124
Posted: 06:31pm 24 Jan 2022
Copy link to clipboard 
Print this post

Another interesting SW option for ESP8266 is Tasmota
Tasmota GitHub

I posted some details about Tasmota in:
ESP8266 AT support in MMBasic
Edited 2022-01-25 04:38 by bigfix
 
lizby
Guru

Joined: 17/05/2016
Location: United States
Posts: 3010
Posted: 11:50pm 24 Jan 2022
Copy link to clipboard 
Print this post

Regarding the long bit of code which Lewis posted, part of it seems to be an implementation of the SPI commands themselves, the same as the MMBasic SPI commands, except this would be MMBasic SPI source code. (I say this not knowing python).

Part of it appears to be asking a connected device to scan for SSIDs--just as "AT+GMR" would for an ESP with AT firmware. But not everything is included. Especially, in "names = self._wait_response_cmd(_SCAN_NETWORKS)", _SCAN_NETWORKS is not defined, and many other constants are likewise not defined.
PicoMite, Armmite F4, SensorKits, MMBasic Hardware, Games, etc. on fruitoftheshed
 
hitsware2

Guru

Joined: 03/08/2019
Location: United States
Posts: 705
Posted: 03:37am 25 Jan 2022
Copy link to clipboard 
Print this post

Would someone (s) elaborate on
the DC(A0?) and the RST ?
my site
 
     Page 1 of 2    
Print this page
© JAQ Software 2024